We’ll continue by defining happly, a heterogeneous apply method. It takes an HList of functions and applies them to the corresponding values in an input HList, producing an HList of results.
First, the example usage looks like:
// data HLists of type Double :: Char :: HNil val y1 = 9.75 :: 'x' :: HNil val y2 = -2.125 :: 'X' :: HNil // The functions to apply. z has type: // // (Double => Double) :: (Char => Boolean) :: HNil // val z = ((_: Double) + .5) :: ( (_: Char).isUpper) :: HNil // apply to first input HList y1 val z1 = happly(z)(y1) // check types val z1Types : Double :: Boolean :: HNil = z1 // check values val 10.25 :: false :: HNil = z1 // apply to second input HList y2 val z2 = happly(z)(y2) // check types val z2Types : Double :: Boolean :: HNil = z2 // check values val -1.625 :: true :: HNil = z2
We’ll implement happly using a type class HApply, which is essentially Function1. We can’t actually use Function1 because existing implicits related to Function1 get in the way.
sealed trait HApply[-In, +Out] { def apply(in: In): Out }
The idea is that given an HList of functions, we produce an HApply that accepts an HList of parameters of the right type and produces an HList of results. For the function `z` from the example, we want an HApply of type:
HApply[ (Double :: Char :: HNil), (Double :: Boolean :: HNil)]
There are two basic cases for this: HNil and HCons. The easy case is mapping an HNil to an HNil, handled by happlyNil.
implicit def happlyNil(h: HNil) : HApply[HNil, HNil] = new HApply[HNil, HNil] { def apply(in: HNil) = HNil }
As usual, HCons is the interesting case. We accept an HCons cell with a head that is a function and produce an HApply that will accept an input HCons cell of the right type. The HApply then applies the head function to the head value and recurses on the tail. The HApply to use for recursion is provided as an implicit and this is how we require that one HList is an HList entirely of functions and the other HList contains values of the right type to be provided as inputs to those functions.
implicit def happlyCons[InH, OutH, TF <: HList, TIn <: HList, TOut <: HList] (implicit applyTail: TF => HApply[TIn, TOut]) = (h: HApply[InH :: TIn, OutH :: TOut]) => new HApply[InH :: TIn, OutH :: TOut] { def apply(in: InH :: TIn) = HCons( h.head(in.head), applyTail(h.tail)(in.tail) ) }
In the example, we have:
val y1 = 9.75 :: 'x' :: HNil val z: (Double => Double) :: (Char => Boolean) :: HNil = ((_: Double) + .5) :: ( (_: Char).isUpper) :: HNil
So, for happly(z)(y1), our implicit is constructed with:
happlyCons[ Double, Double, Char => Boolean :: HNil, Char :: HNil, Boolean :: HNil]( happlyCons[ Char, Boolean, HNil, HNil, HNil]( happlyNil ) )
The first applyCons constructs an HApply that uses the head of z, a function of type `Double => Double`, to map an HList with a head of type Double to an HList with a head of type Double. It uses the HApply from the second happlyCons for mapping the tail of the input HList.
This second HApply uses the second element of z, a function of type `Char => Boolean`, to map an HList with a head of type Char to an HList with a head of type Boolean. Because this is the last element, the recursion ends with happlyNil mapping HNil to HNil.
Finally, we define an entry point. Given an HList of functions and an HList of arguments to those functions, we use happly to grab an HApply implicitly and produce the resulting HList with it:
def happly[H <: HList, In <: HList, Out <: HList] (h: H)(in: In)(implicit toApply: H => HApply[In, Out]): Out = toApply(h)(in)
I keep ready happly as happily. Which nicely describes the manner in which I am absorbing this series.
Which existing implicits get in the way of using Function1 rather than HApply?
In general, I find that using a custom type helps to isolate the implicits and makes error messages clearer and debugging easier.
This time, it turned out to be one unrelated to this series (it was a failed experiment related to inferring the least upper bound for converting to a plain List), so I updated the code on github to use Function1. It is cleaner since it can use function literals instead of anonymous classes.
http://github.com/harrah/up/blob/master/HApply.scala
http://github.com/harrah/up/blob/master/HApplyTest.scala
Hi,
Starting from your great articles, I probably succeeded in removing implicits.
My approach is found at: http://bit.ly/dzANIQ
To be honest, I implemented Lift1 just now!
Regards,
It is an interesting approach and you should write it up. I’d be interested to know how it should be used properly. For example, I tried your test case, but I want the following to be a compile error and not a runtime error:
scala> val y1 = Box(9.75) :: Box('x') :: Nil
y1: com.github.okomok.mada.dual.list.Cons[com.github.okomok.mada.dual.Box[Double],com.github.okomok.mada.dual.list.Cons[com.github.okomok.mada.dual.Box[Char],com.github.okomok.mada.dual.list.Nil]] = dual.List(9.75, x)
scala> val z = Lift1((_: Double) + .5) :: Box(3) :: Nil
z: com.github.okomok.mada.dual.list.Cons[com.github.okomok.mada.dual.Lift1[Double,Double],com.github.okomok.mada.dual.list.Cons[com.github.okomok.mada.dual.Box[Int],com.github.okomok.mada.dual.list.Nil]] = dual.List(, 3)
scala> z.zipBy(y1, Apply).force
java.lang.UnsupportedOperationException: dual.Any.asFunction1
at com.github.okomok.mada.dual.Any$class.unsupported(Any.scala:109)
at com.github.okomok.mada.dual.Box.unsupported(Box.scala:14)
at com.github.okomok.mada.dual.Any$class.asFunction1(Any.scala:29)
at com.github.okomok.mada.dual.Box.asFunction1(Box.scala:14)
...
The equivalent using HApply is:
scala> val y1 = 9.75 :: 'x' :: HNil
y1: HCons[Double,HCons[Char,HNil]] = 9.75 :: x :: HNil
scala> val z = ((_: Double) + .5) :: 3 :: HNil
z: HCons[(Double) => Double,HCons[Int,HNil]] = :: 3 :: HNil
scala> happly(z)(y1)
error: could not find implicit value for parameter toApply: (HCons[(Double) => Double,HCons[Int,HNil]]) => (HCons[Double,HCons[Char,HNil]]) => Out
happly(z)(y1)
^
Certainly we’d like the error message to be more helpful in each case.
Yes, `f#asFunction1` unfortunately compiles.
This is the millennium problem for me.
A workaround will be an `implicit` after all :(
I wish I had the type-level `throw`…
Regards,
In Scala 2.9, you can customize error messages for implicit not found errors. http://github.com/scala/scala/blob/master/src/library/scala/annotation/implicitNotFound.scala#L18
(WP won’t let me reply to your replies, so here they are…)
@Okomok
Perhaps write about your system and why it should work or what the obstacles are to making it work. You might get useful feedback from readers.
@Retronym
That annotation has to go on the type being searched for, though. So, you’d get the message for Function1 or you’d have to have custom versions of Function1 just to define your own message:
http://lampsvn.epfl.ch/trac/scala/ticket/2462
Even then, I think it would be hard to get an error message like:
type mismatch in the second element of z.
Expected: Char => ?
Got: Int
> Perhaps write about your system…
Sadly it might be too difficult for my poor English.
(First I should write an article by my native language.)
BTW, the origin of my system is `trait Fold[-Elem, Value]` of your `up` library.
It inspired me Scala type-system probably has the same power as C++.
In fact, `mada.dual` is intended to port Boost.Fusion, which too consists of
pairs of type-level method and generic method.
Regards,
There seems to be an error in the definition of happlyCons – instead of `(h: HApply[InH :: TIn, OutH :: TOut]) =>`, it should contain `(h: ((InH => OutH) :: TF)) =>`. I confirmed this by inspection of https://github.com/harrah/up/blob/master/HApply.scala (after factoring in the necessary changes).