I’m sure you’ve written code like this before. You have a case class …
Foo(b: Boolean, i: Int)
… and a function that has the same arguments as the case class fields.
def showFoo(b: Boolean, i: Int)
And you need to write a function that unpacks the case class fields and passes them into the function.
def receive(f: Foo) = showFoo(f.b, f.i)
That doesn’t look so bad, but imagine Foo has 5 fields. Do you really want to write showFoo(f.b, f.i, f.a, f.c, f.d)
?
First steps
Luckily, we can convert a function that takes many arguments into one that takes a tuple of those arguments:
val showFooTuple = (showFoo _).tupled
And we can also convert a case class to a tuple.
val foo = Foo(true, 1)
val fooTuple = Foo.unapply(foo).get
So now we can write receive as
receive(f: Foo) = (showFoo _).tupled.apply(Foo.unapply(foo).get)
Hmm, that doesn’t look great. Let’s write a helper function that applies a function to a Foo.
def applyFoo[R](f: (Boolean, Int) => R, f: Foo) = f.tupled.apply(Foo.unapply(foo).get)
Now our receive function is more readable:
receive(f: Foo) = applyFoo(showFoo)(f)
applyFoo
does what we want it to do for Foo, but we have dozens of case classes. Can we write a function that works for arbitrary ones? Unfortunately, the approach above won’t get us anywhere. How are we supposed to do Foo.unapply generically?
A generic solution
Shapeless to the rescue! Shapeless has generic functions that allow you to convert case classes into HLists as well as converting function so that they take HLists as arguments.
import shapeless._
import shapeless.ops.function.FnToProduct
def applyGeneric[
C, // our case class
H <: HList, // the HList representation of the case class
R, // the return type of the function
F] // the type of the function
(f: F, c: C) // the function f and the case class instance c
(implicit gc: Generic[C]{ type Repr <: H }, // conversion of the case class to the HList
fnHLister: FnToProduct.Aux[F, H => R]) // conversion of the function
: R =
fnHLister(f)(gc.to(c)) // convert the function, convert the case class instance, apply
Looks scary, doesn’t it? Fortunately, we only have to write this monstrosity once. Client code doesn’t need to know about shapeless or HList
s and requires no magic imports.
applyGeneric(showFoo _, foo)
Why the _?
Scala only converts def
s to functions when a function type is required and the first parameter to applyGeneric
has type F
, which could be any type. We can make the type more specific, but then we have to write one function for every arity, replacing F
with (P1, P2, ... Pn) => R
.
def ag2[C, H <: HList, R, P1, P2]
(f: (P1, P2) => R, c: C)
(implicit gc: Generic[C]{ type Repr <: H },
fnHLister: FnToProduct.Aux[(P1, P2) => R, H => R]): R =
fnHLister(f)(gc.to(c))
def ag3[C, H <: HList, R, P1, P2, P3]
(f: (P1, P2, P3) => R, c: C)
(implicit gc: Generic[C]{ type Repr <: H },
fnHLister: FnToProduct.Aux[(P1, P2, P3) => R, H => R]): R =
fnHLister(f)(gc.to(c))
build.sbt
Compiled with sbt 1.0, Scala 2.12, and Shapeless 2.3.3
scalaVersion := "2.12.4"
scalacOptions ++= Seq("-feature", "-language:_", "-Ypartial-unification")
resolvers ++= Seq (
Resolver.sonatypeRepo("releases"),
Resolver.sonatypeRepo("snapshots")
)
libraryDependencies ++= Seq("com.chuusai" %% "shapeless" % "2.3.3")