Skip to content

Instantly share code, notes, and snippets.

@jprudent
Last active February 17, 2023 16:02
Show Gist options
  • Save jprudent/d022b12ebfc9e67416c976cb407a212f to your computer and use it in GitHub Desktop.
Save jprudent/d022b12ebfc9e67416c976cb407a212f to your computer and use it in GitHub Desktop.
Using Type Classes to Solve the Expression Problem
// 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)
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
// 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