Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shapeless map HList depending on target types

I have the following problem, I want to map items of an HList to another HList but Strings in the source HList should only be converted to URL if the "target" type is URL.

val name = "Stackoverflow"
val url = "https://stackoverflow.com/q"
val list = name :: url :: HNil

val mapped: String :: URL :: HNil = list.map(???)

As far as my research took me is that all the Poly stuff only cares about the input type but not about the output type. So are there ways to archive my goal ?

like image 625
shim_ Avatar asked Apr 09 '16 11:04

shim_


1 Answers

I don't think you're going to get exactly what you want, since Scala's implicit resolution happens before type inference (but who knows—people do things that surprise me in Scala all the time).

(Side note: the CanBuildFrom / breakOut pattern supports something similar to what you're asking for, but I don't see a way to make it work in this situation, since the source type does constrain what instances are available.)

There is a pretty standard workaround for this kind of situation, though, involving the use of a helper class to approximate partial application of type parameters. Suppose you've got a fairly straightforward type class that captures your conversion logic:

import java.net.URL
import shapeless._

trait Convert[I <: HList, O <: HList] { def apply(i: I): O }

object Convert extends LowPriorityConvertInstances {
  implicit val convertHNil: Convert[HNil, HNil] = new Convert[HNil, HNil] {
    def apply(i: HNil): HNil = i
  }

  implicit def convertHConsURL[T <: HList, TO <: HList](implicit
    c: Convert[T, TO]
  ): Convert[String :: T, URL :: TO] = new Convert[String :: T, URL :: TO] {
    def apply(i: String :: T): URL :: TO = new URL(i.head) :: c(i.tail)
  }
}

sealed class LowPriorityConvertInstances {
  implicit def convertHCons[H, T <: HList, TO <: HList](implicit
    c: Convert[T, TO]
  ): Convert[H :: T, H :: TO] = new Convert[H :: T, H :: TO] {
    def apply(i: H :: T): H :: TO = i.head :: c(i.tail)
  }
}

Now you might try something like this:

def convert[I <: HList, O <: HList](i: I)(implicit c: Convert[I, O]): O = c(i)

But there are two problems here. The first is that if you let the type parameters be inferred, you'll always get a conversion that turns every string into a URL. You can override this behavior by explicitly providing both type parameters, but ugh.

We can (kind of) improve this situation with a helper class:

class PartiallyAppliedConvert[O <: HList] {
  def apply[I <: HList](i: I)(implicit c: Convert[I, O]): O = c(i)
}

def convert[O <: HList]: PartiallyAppliedConvert[O] =
  new PartiallyAppliedConvert[O]

Now you can write the following:

scala> val mapped = convert[String :: URL :: HNil](list)
mapped: shapeless.::[String,shapeless.::[java.net.URL,shapeless.HNil]] = Stackoverflow :: https://stackoverflow.com/q :: HNil

It's not exactly what you asked for, but it's pretty close, in that the only type we have to specify explicitly is the desired target type.

like image 165
Travis Brown Avatar answered Sep 20 '22 07:09

Travis Brown