Skip to content

Instantly share code, notes, and snippets.

@jmccance
Last active July 27, 2017 18:39
Show Gist options
  • Save jmccance/d6009d23a11a593ddb5e9aba49b1a11a to your computer and use it in GitHub Desktop.
Save jmccance/d6009d23a11a593ddb5e9aba49b1a11a to your computer and use it in GitHub Desktop.
Deriving Show with Shapeless
name := "shapeless-lightning-talk"
scalaVersion := "2.12.2"
libraryDependencies ++= Seq(
"com.chuusai" %% "shapeless" % "2.3.2"
)
package examples
import shapeless._
import shapeless.labelled.FieldType
// First we need a typeclass to demo shapeless with. This one just handles converting things
// to strings.
trait Show[A] {
def apply(a: A): String
}
object Show {
// Add a `show` extension method to classes so that we can more easily run the pretty-printer on
// it.
implicit class ShowOps[A](val a: A) extends AnyVal {
def show(implicit showA: Show[A]): String = showA(a)
}
// Derive some instances for Double, String, and List.
implicit val showDouble: Show[Double] = x => x.toString
implicit val showString: Show[String] = s => s.toString
implicit def showList[A](implicit showA: Show[A]): Show[List[A]] =
as => as.map(showA(_)).mkString("[\n ", ",\n ", "\n]")
// Next we'll implement some instances to handle product types.
// The base case is just the empty string; we don't care about rendering empty products.
implicit val showHNil: Show[HNil] = _ => ""
// Now we'll render an HList as comma-separated values. `K` is the "key" of the labels, which are
// just symbols (`'foo`, `'bar`, etc.). The HList is an HList of FieldTypes, giving them their
// labels.
implicit def showHList[K <: Symbol, H, T <: HList](implicit
witness: Witness.Aux[K],
showH: Show[H],
showT: Show[T]): Show[FieldType[K, H] :: T] = {
// Unpack the field name out of the witness.
val fieldName = witness.value.name
{
case h :: HNil => s"$fieldName = ${showH(h)}"
case h :: t => s"$fieldName = ${showH(h)}, ${showT(t)}"
}
}
// Next, ADTs. Again, the base case is just the empty string.
implicit val showCNil: Show[CNil] = _ => ""
// The coproduct case is also labeled, with the same basic structure as the HList.
implicit def showCoproduct[K <: Symbol, L, R <: Coproduct](implicit
witness: Witness.Aux[K],
showL: Lazy[Show[L]],
showR: Show[R]): Show[FieldType[K, L] :+: R] = {
val typeName = witness.value.name
{
case Inl(left) => s"$typeName ${showL.value(left)}"
case Inr(right) => showR(right)
}
}
// Separate generic derivers for case classes and ADTs allows us to wrap the case classes in
// parens. Not sure how to render them as (e.g.) `Point(x = 1.0, y = 1.0)`, but the parens at
// least make it clearer.
implicit def showCaseClass[A, R <: HList](implicit
genericA: LabelledGeneric[A] {type Repr = R},
showR: Lazy[Show[R]]): Show[A] = { a =>
s"(${showR.value(genericA.to(a))})"
}
implicit def showAdt[A, R <: Coproduct](implicit
genericA: LabelledGeneric[A] {type Repr = R},
showR: Lazy[Show[R]]): Show[A] = { a =>
showR.value(genericA.to(a))
}
}
object Shapes extends App {
import Show.ShowOps
case class Point(x: Double, y: Double)
sealed trait Shape extends Product with Serializable
case class Circle(radius: Double, origin: Point) extends Shape
case class Rectangle(lowerLeft: Point, upperRight: Point) extends Shape
println(
List(
Circle(1.0, Point(0.0, 0.0)),
Rectangle(Point(-0.5, -0.5), Point(0.5, 0.5))
).show
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment