tag:blogger.com,1999:blog-53692468781189275662024-03-13T21:51:19.171-07:00Kim Stebel's BlogI'm not mad, I'm just disappointedUnknownnoreply@blogger.comBlogger1125tag:blogger.com,1999:blog-5369246878118927566.post-26777877357157582342018-02-11T05:04:00.001-08:002018-02-11T05:30:35.792-08:00How to apply functions to case classes with Shapeless<p>I’m sure you’ve written code like this before. You have a case class …</p>
<pre><code class="scala">Foo(b: Boolean, i: Int)
</code></pre>
<p>… and a function that has the same arguments as the case class fields.</p>
<pre><code class="scala">def showFoo(b: Boolean, i: Int)
</code></pre>
<p>And you need to write a function that unpacks the case class fields and passes them into the function.</p>
<pre><code class="scala">def receive(f: Foo) = showFoo(f.b, f.i)
</code></pre>
<p>That doesn’t look so bad, but imagine Foo has 5 fields. Do you really want to write <code class="scala">showFoo(f.b, f.i, f.a, f.c, f.d)</code>?</p>
<h2 id="first-steps">First steps</h2>
<p>Luckily, we can convert a function that takes many arguments into one that takes a tuple of those arguments:</p>
<pre><code class="scala">val showFooTuple = (showFoo _).tupled
</code></pre>
<p>And we can also convert a case class to a tuple.</p>
<pre><code class="scala">val foo = Foo(true, 1)
val fooTuple = Foo.unapply(foo).get
</code></pre>
<p>So now we can write receive as</p>
<pre><code class="scala">receive(f: Foo) = (showFoo _).tupled.apply(Foo.unapply(foo).get)
</code></pre>
<p>Hmm, that doesn’t look great. Let’s write a helper function that applies a function to a Foo.</p>
<p>def applyFoo[R](f: (Boolean, Int) => R, f: Foo) = f.tupled.apply(Foo.unapply(foo).get)</p>
<p>Now our receive function is more readable:</p>
<pre><code class="scala">receive(f: Foo) = applyFoo(showFoo)(f)
</code></pre>
<p><code class="scala">applyFoo</code> 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?</p>
<h2 id="a-generic-solution">A generic solution</h2>
<p><a href="https://github.com/milessabin/shapeless">Shapeless</a> to the rescue! Shapeless has generic functions that allow you to convert case classes into <a href="https://wiki.haskell.org/Heterogenous_collections">HLists</a> as well as converting function so that they take HLists as arguments.</p>
<pre><code class="scala">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
</code></pre>
<p>Looks scary, doesn’t it? Fortunately, we only have to write this monstrosity once. Client code doesn’t need to know about shapeless or <code class="scala">HList</code>s and requires no magic imports.</p>
<pre><code class="scala">applyGeneric(showFoo _, foo)
</code></pre>
<h2 id="why-the-_">Why the _?</h2>
<p>Scala only converts <code class="scala">def</code>s to functions when a function type is required and the first parameter to <code class="scala">applyGeneric</code> has type <code class="scala">F</code>, which could be any type. We can make the type more specific, but then we have to write one function for every arity, replacing <code class="scala">F</code> with <code class="scala">(P1, P2, ... Pn) => R</code>.</p>
<pre><code class="scala">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))
</code></pre>
<h2 id="build.sbt">build.sbt</h2>
<p>Compiled with sbt 1.0, Scala 2.12, and Shapeless 2.3.3</p>
<pre><code class="scala">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")
</code></pre>Unknownnoreply@blogger.com0