Search code examples
scalashapeless

Using a polymorphic function to extract an object from Options


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?


Solution

  • 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.