Last active
May 2, 2017 19:40
-
-
Save slouc/4deeddab2b93b6f08e0086cff845dcd5 to your computer and use it in GitHub Desktop.
ShapelessGenericTypeClassDerivation
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
import shapeless._ | |
object Basic { | |
case class Person(firstName: String, age: Int, married: Boolean) | |
case class Band(name: String, numOfAlbums: Int, stillActive: Boolean) | |
def personSerialized(p: Person): List[String] = List(p.firstName, p.age.toString, p.married.toString) | |
def bandSerialized(b: Band): List[String] = List(b.name, b.numOfAlbums.toString, b.stillActive.toString) | |
val genericPerson: shapeless.::[String, shapeless.::[Int, shapeless.::[Boolean, shapeless.HNil]]] = | |
Generic[Person].to(Person("David", 29, false)) | |
val genericBand: shapeless.::[String, shapeless.::[Int, shapeless.::[Boolean, shapeless.HNil]]] = | |
Generic[Band].to(Band("Muse", 7, true)) | |
def genericSerialized(gen: String :: Int :: Boolean :: HNil): List[String] = | |
List(gen(0), gen(1).toString, gen(2).toString) | |
val gp: List[String] = genericSerialized(genericPerson) // List(David, 29, false) | |
val gb: List[String] = genericSerialized(genericBand) // List(Muse, 7, true) | |
} | |
object TypeClasses { | |
import Basic._ | |
trait SerializedEncoder[A] { | |
def encode(value: A): List[String] | |
} | |
object SerializedEncoder { | |
def apply[A](implicit enc: SerializedEncoder[A]): SerializedEncoder[A] = enc | |
} | |
// These two encoders below are specific for each product type. | |
implicit val personEncoder: SerializedEncoder[Person] = | |
new SerializedEncoder[Person] { | |
def encode(p: Person) = List(p.firstName, p.age.toString, if (p.married) "yes" else "no") | |
} | |
implicit val bandEncoder: SerializedEncoder[Band] = | |
new SerializedEncoder[Band] { | |
def encode(b: Band) = List(b.name, b.numOfAlbums.toString, if (b.stillActive) "yes" else "no") | |
} | |
// 1. If we have type class instances for the head and tail of an HList, | |
// we can derive an instance for the whole HList. | |
// 2. If we have a case class A, a Generic[A], and a type class instance for the generic’s Repr, | |
// we can combine them to create an instance for A. | |
def createEncoder[A](func: A => List[String]): SerializedEncoder[A] = | |
new SerializedEncoder[A] { | |
def encode(value: A): List[String] = func(value) | |
} | |
implicit val stringEncoder: SerializedEncoder[String] = createEncoder(str => List(str)) | |
implicit val intEncoder: SerializedEncoder[Int] = createEncoder(num => List(num.toString)) | |
implicit val booleanEncoder: SerializedEncoder[Boolean] = createEncoder(bool => List(if (bool) "yes" else "no")) | |
implicit val hnilEncoder: SerializedEncoder[HNil] = createEncoder(hnil => Nil) | |
implicit def hlistEncoder[H, T <: HList](implicit | |
hEncoder: SerializedEncoder[H], | |
tEncoder: SerializedEncoder[T] | |
): SerializedEncoder[H :: T] = | |
createEncoder { | |
case h :: t => hEncoder.encode(h) ++ tEncoder.encode(t) | |
} | |
// Taken together, these five instances allow us to summon SerializedEncoders | |
// for any HList involving Strings, Ints, and Booleans. | |
// First let's create one for Person, then we'll do the really generic one. | |
implicit val genericPersonEncoder: SerializedEncoder[Person] = { | |
val gen = Generic[Person] | |
val enc = SerializedEncoder[gen.Repr] | |
createEncoder(person => enc.encode(gen.to(person))) | |
} | |
// Let's now follow the same principle for generic encoder. | |
// | |
// First, we go from A to generic representation of A (e.g. a HList). | |
// We also know that we have an encoder for *any generic* repr (with the help of those five implicit encoders). | |
// We can use that implicitly available encoder to turn our generic repr of A into a List[String]. | |
// | |
// What we just described is a function A => List[String]. Once we have that function, it's easy to construct | |
// a SerializedEncoder[A] since that's all it needs (we can use the utility function createEncoder for this): | |
implicit def genericEncoder[A, R](implicit | |
gen: Generic[A] {type Repr = R}, | |
enc: SerializedEncoder[R]): SerializedEncoder[A] = | |
createEncoder(a => enc.encode(gen.to(a))) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment