HList with unary constrain to varargs cast

518 views Asked by At

Let's say we have this:

def join[A, B](a: A, b: B)
def join[A, B, C](a: A, b: B, c: C) // etc

Basically a lot of overloaded methods for up to 20 type arguments.

And then the following which is creating a K-list by enforcing an unary type constraint on the HList such that all inner elements are of type Task[_].

class Test(L <: HList : *->*[Task]#λ](val deps : L = HNil) 

Is it possible to convert the deps to a Task[A1, A2, A3] etc where the A1, A2, A3 are the inner types of the HList?

So for:

val hls = Task(1) :: Task("string") :: Task9(true) :: HNil

Retrieve a:

Task[(Int, String, Boolean)]

And do this for any number of arguments? I can convert a Task[A], Task[B] to a Task[A, B] for any number of arguments already, just need to get from HList to varargs or similar.

1

There are 1 answers

3
Travis Brown On BEST ANSWER

From Scala's perspective those join methods don't have anything in common but their name, so it's not really possible to use them in this context without a lot of boilerplate (or a custom macro). Instead it's possible to use the join on Twitter's Future itself repeatedly. You can do this by creating a new type class:

import shapeless._

trait FutureSequence[L <: HList] extends DepFn1[L]

This type class will witness that an hlist L is made up entirely of futures, and it will give us a way to sequence those futures into a Future[T], where T is the tuple made up of the types inside of each of the futures in L (the *->* gives us the first part of this, but doesn't support the second, or even any way to do the second part at all conveniently).

The companion object does all the interesting work:

import com.twitter.util.Future
import shapeless.ops.tuple.Prepend

object FutureSequence {
  type Aux[L <: HList, Out0] = FutureSequence[L] { type Out = Out0 }

  implicit def hnilFutureSequence: Aux[HNil, Future[Unit]] =
    new FutureSequence[HNil] {
      type Out = Future[Unit]
      def apply(l: HNil): Future[Unit] = Future.Unit
    }

  implicit def hconsFutureSequence[H, T <: HList, OutT](implicit
    fst: Aux[T, Future[OutT]],
    pre: Prepend[Tuple1[H], OutT]
  ): Aux[Future[H] :: T, Future[pre.Out]] = new FutureSequence[Future[H] :: T] {
    type Out = Future[pre.Out]

    def apply(l: Future[H] :: T): Future[pre.Out] =
      l.head.join(fst(l.tail)).map {
        case (h, t) => pre(Tuple1(h), t)
      }
  }
}

We're using induction here—first we describe how to sequence HNil (our base case), and then we describe how to sequence Future[H] :: T given that we know how to sequence T.

Next we'll define a method to make this easy to use:

def sequence[L <: HList](l: L)(implicit fs: FutureSequence[L]): fs.Out = fs(l)

And then if we've got some examples:

 val good = Future(1) :: Future("string") :: Future(true) :: HNil
 val bad = Future(1 / 0) :: Future("string") :: Future(true) :: HNil

We can sequence them:

scala> import com.twitter.util.Await
import com.twitter.util.Await

scala> Await.result(sequence(good))
res0: (Int, String, Boolean) = (1,string,true)

scala> sequence(bad).onFailure(println)
java.lang.ArithmeticException: / by zero
res1: com.twitter.util.Future[(Int, String, Boolean)] = ...

You could also require a FutureSequence instance in your class's constructor, or on specific methods on your class.