The shapeless documentation explains how to use polymorphic functions to make a function that maps objects in one kind of container to another, but what about when you want to unpack things from their container?
I have a HList of Options
val options = Some(1) :: Some("A") :: Some(3.5) :: HNil
I want a polymorphic function that can extract the contents of each of the Options.
// This is incorrect:
object uuu extends (Option ~> Any) {
def apply[T](l:Option[T]):T = {
l.get
}
}
If this function was correct, I'd want the following behavior:
options.map(uuu) // I want: 1 :: "A" :: 3.5 :: HNil
How can I correct this so that my polymorphic function actually works?
There are a couple of problems here. The first is that the static type of your hlist has Some
in it instead of Option
, so the Mapper
evidence that you need to prove that uuu
can be mapped over options
won't be found. The nicest way to fix this is to define a smart Some
constructor that returns an Option
:
def some[A](a: A): Option[A] = Some(a)
val options = some(1) :: some("A") :: some(3.5) :: HNil
You could also add type annotations to your original options
, or change uuu
to work with Some
instead of Option
(which would be safer, but presumably less useful for whatever it is you're aiming to do).
Now your code compiles and does something, but only because of the somewhat bizarre fact that Any
is kind-polymorphic in Scala. In general when you have F ~> G
, both F
and G
have to be type constructors that take a single type parameter—e.g. Option ~> List
. Any
doesn't take a type parameter, and yet it works, because of this weird fact about the Scala language (that Any
, together with Nothing
, is kind-polymorphic and will fit any slot where you need a type, a type constructor with one parameter, a type constructor with a dozen parameters, etc.).
So it compiles, but it's pretty useless, since it returns an Any :: Any :: Any :: HNil
. You can fix this by replacing the Any
in the natural transformation with shapeless.Id
:
import shapeless._, shapeless.poly.~>
def some[A](a: A): Option[A] = Some(a)
val options = some(1) :: some("A") :: some(3.5) :: HNil
object uuu extends (Option ~> Id) {
def apply[T](l: Option[T]): T = l.get
}
options.map(uuu)
Id
is defined as type Id[+T] = T
—i.e., it's the identity type constructor that gives you the unwrapped type.
This version both compiles and gives you back a usefully-typed result, but it's still not really safe, since if you map over an hlist
with elements that are None
(at runtime), you'll get a NoSuchElementException
. There's not really any way around this apart from changing Option ~> Id
to Some ~> Id
, somehow providing default values, etc., all of which change the nature of the operation pretty dramatically.