Last active
February 17, 2023 16:02
-
-
Save jprudent/d022b12ebfc9e67416c976cb407a212f to your computer and use it in GitHub Desktop.
Using Type Classes to Solve the Expression Problem
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// you can't modify shipped code | |
object VendorLib { | |
trait Json | |
case class JsonString(get: String) extends Json | |
case class JsonObject(get: Map[String, Json]) extends Json | |
case class JsonArray(get: List[Json]) extends Json | |
def spitStr(json: Json): String = ??? | |
} | |
// you can't modify shipped code | |
object UserLib1 { | |
trait Alive | |
case class Human(name: String, family: List[Human]) extends Alive | |
case class Dog(name: String) extends Alive | |
import VendorLib.* | |
def toJson1(a: Alive): Json = a match | |
case Human(name, family) => JsonObject(Map("name" -> JsonString(name), "family" -> JsonArray(family.map(toJson1)))) | |
case Dog(name) => JsonObject(Map("name" -> JsonString(name))) | |
} | |
import UserLib1.* | |
import VendorLib.Json | |
val titi = Human("titi", List()) | |
val jerome = Human("jerome", List(titi)) | |
toJson1(jerome) | |
object UserLib2 { | |
import UserLib1.* | |
case class Slug(color: String) extends Alive | |
import VendorLib.* | |
def toJson2(a: Alive): Json = a match | |
case Human(name, family) => JsonObject(Map("name" -> JsonString(name), "family" -> JsonArray(family.map(toJson1)))) | |
case Dog(name) => JsonObject(Map("name" -> JsonString(name))) | |
case Slug(name) => JsonObject(Map("color" -> JsonString(name))) | |
} | |
import UserLib2.* | |
val slug = Slug("red") | |
//toJson1(slug) // match is not exhaustive | |
// I CAN'T support new type on existing functionality | |
// I CAN add new functionality on existing types | |
toJson2(slug) | |
toJson2(jerome) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object VendorLib { | |
trait Json | |
case class JsonString(get: String) extends Json | |
case class JsonObject(get: Map[String, Json]) extends Json | |
case class JsonArray(get: List[Json]) extends Json | |
def spitStr(json: Json): String = ??? | |
} | |
object UserLib1 { | |
import VendorLib.* | |
trait Encoder { | |
def toJson: Json | |
} | |
trait Alive extends Encoder | |
case class Human(name: String, family: List[Human]) extends Alive { | |
override def toJson: Json = JsonObject(Map("name" -> JsonString(name), "family" -> JsonArray(family.map(_.toJson)))) | |
} | |
case class Dog(name: String) extends Alive { | |
override def toJson: Json = JsonObject(Map("name" -> JsonString(name))) | |
} | |
} | |
import UserLib1.* | |
import VendorLib.Json | |
val titi = Human("titi", List()) | |
val jerome = Human("jerome", List(titi)) | |
jerome.toJson | |
object UserLib2 { | |
import UserLib1.* | |
import VendorLib.* | |
trait Encoder2 { | |
def toJson2: Json | |
} | |
case class Slug(color: String) extends Alive with Encoder2 { | |
override def toJson: Json = JsonObject(Map("color" -> JsonString(color))) | |
override def toJson2: Json = JsonObject(Map("color" -> JsonString(color))) | |
} | |
} | |
// I CAN support new type on existing functionality | |
// I CAN'T add new functionality on existing types | |
import UserLib2.* | |
val slug = Slug("red") | |
slug.toJson | |
slug.toJson2 | |
//jerome.toJson2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// expression problem: | |
// I WANT TO add new type on existing functionality | |
// I WANT TO add new functionality on existing types | |
object VendorLib { | |
trait Json | |
case class JsonString(get: String) extends Json | |
case class JsonObject(get: Map[String, Json]) extends Json | |
case class JsonArray(get: List[Json]) extends Json | |
def spitStr(json: Json): String = ??? | |
} | |
// type class: | |
// 1) Definir le type class | |
// 2) Instancier la type class | |
// 3) Utiliser la type class | |
object VendorFeature { | |
import VendorLib.* | |
trait Encoder[A] { | |
def toJson1(a: A): Json | |
} | |
} | |
object UserLib1 { | |
trait Alive | |
case class Human(name: String, family: List[Human]) extends Alive | |
case class Dog(name: String) extends Alive | |
import VendorFeature.* | |
import VendorLib.* | |
val humanEncoder = new Encoder[Human] { | |
override def toJson1(a: Human): Json = JsonObject( | |
Map("name" -> JsonString(a.name), "family" -> JsonArray(a.family.map(toJson1))) | |
) | |
} | |
given Encoder[Human] = humanEncoder | |
given Encoder[Dog] = (a: Dog) => JsonObject(Map("name" -> JsonString(a.name))) | |
import VendorLib.* | |
} | |
import UserLib1.* | |
import UserLib1.toJson1 | |
import VendorFeature.Encoder | |
import VendorLib.Json | |
val titi = Human("titi", List()) | |
val jerome = Human("jerome", List(titi)) | |
val medor = Dog("fluggy") | |
humanEncoder.toJson1(jerome) | |
//dogEncoder.toJson1(medor) | |
// 3) use type class with "object interface" | |
object Serializer { | |
import VendorFeature.* | |
import VendorLib.* | |
def toJson1[A](a: A)(using encoder: Encoder[A]): Json = encoder.toJson1(a) | |
} | |
Serializer.toJson1(jerome) | |
Serializer.toJson1(medor) | |
object UserLib2 { | |
import UserLib1.* | |
case class Slug(color: String) extends Alive | |
import VendorLib.* | |
val slugEncoder = new Encoder[Slug] { | |
override def toJson1(a: Slug): Json = JsonObject( | |
Map("color" -> JsonString(a.color)) | |
) | |
} | |
given Encoder[Slug] = slugEncoder | |
} | |
import UserLib2.* | |
val slug = Slug("red") | |
Serializer.toJson1(slug) | |
object VendorFeature2 { | |
import VendorLib.* | |
trait Encoder2[A] { | |
def toJson2(a: A): Json | |
} | |
val humanEncoder = new Encoder2[Human] { | |
override def toJson2(a: Human): Json = JsonObject( | |
Map("name" -> JsonString(a.name), "family" -> JsonArray(a.family.map(toJson2))) | |
) | |
} | |
given Encoder2[Human] = humanEncoder | |
given Encoder2[Dog] = (a: Dog) => JsonObject(Map("name" -> JsonString(a.name))) | |
val slugEncoder = new Encoder2[Slug] { | |
override def toJson2(a: Slug): Json = JsonObject( | |
Map("color" -> JsonString(a.color)) | |
) | |
} | |
given Encoder2[Slug] = slugEncoder | |
} | |
// Order of resolution of implicits: | |
// meme bloc | |
// parameter fn | |
// import specifi | |
// import * | |
// companion object | |
// companion type class <-- This is the one we like to use | |
// | |
import VendorFeature2.* | |
// 3) use type class with "syntax interface" | |
extension [A](a: A) def toJson2(using encoder: Encoder2[A]) = encoder.toJson2(a) | |
jerome.toJson2 | |
slug.toJson2 | |
medor.toJson2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment