scala 3 map tuple to futures of tuple types and back

321 Views Asked by At

I'm trying to take an arbitrary tuple of Futures and return a tuple of the completed future's values, while providing a time limit for the completion of the futures. I'm trying to use Tuple's provided Map match type:


    def getAll[T <: Tuple](futures: Tuple.Map[T, Future])(timeout: Long, units: TimeUnit): T = futures match
        case e: EmptyTuple => EmptyTuple.asInstanceOf[T]
        case fs: (fh *: ft) => // here, I'd think `fh` would be known to be a Future, specifically Head[Map[T, Future]], and `ft` a Map[Tail[T], Future]
            val start = System.currentTimeMillis()
            val vh = fs.head.asInstanceOf[fh].get(timeout, units)
            val elapsed = System.currentTimeMillis() - start
            val remaining = TimeUnit.MILLISECONDS.convert(timeout, units) - elapsed

            vh *: getAll(fs.tail)(remaining)

But I'm getting an error:

value get is not a member of fh

where:    fh is a type in method getAll with bounds 

            val vh = fs.head.asInstanceOf[fh].get(timeout, units)

It seems like the compiler can't tell that fh is a Future. I'm trying to follow guidance on match types I got from a previous question of mine, in particular trying to match the value patterns with the match type patterns, but I guess am still missing something.

Any ideas?

EDIT: I got to this version that at least compiles and seems to run properly:

    def getAll[T <: Tuple](futures: Tuple.Map[T, Future])(timeout: Long, units: TimeUnit): T = futures match
        case _: EmptyTuple => EmptyTuple.asInstanceOf[T]
        case fs: Tuple.Map[fh *: ft, Future] =>
            val start = System.nanoTime()
            val vh = fs.head.asInstanceOf[Future[fh]].get(timeout, units)
            val elapsed = System.nanoTime() - start
            val remaining = TimeUnit.NANOSECONDS.convert(timeout, units) - elapsed

            (vh *: getAll(fs.tail)(remaining, TimeUnit.NANOSECONDS)).asInstanceOf[T]

but a) with the warning:

  scala.Tuple.Map[ft, java.util.concurrent.Future]
) @ft @fh cannot be checked at runtime
        case fs: Tuple.Map[fh *: ft, Future] =>

which makes sense, I just don't know how to fix it. I'm guessing if I could somehow conjure a ClassTag[ft], but that doesn't seem possible...

and b) the usage needs a type ascription, which makes it much less useable, for example:

getAll[(String, String)]((f1, f2))(to, TimeUnit.SECONDS) // doesn't compile without [(String, String)] 

Scastie here

1

There are 1 best solutions below

2
On

In match types case Tuple.Map[fh *: ft, Future] can make sense. But in pattern matching case fs: Tuple.Map[fh *: ft, Future] is just case fs: Tuple.Map[_, _] because of type erasure.

Currently on value level match types work not so well (many things can't be inferred). Good old type classes can be better.

I guess you meant Await.result instead of not existing Future.get.

Try to make the method inline and add implicit hints summonFrom { _: some_evidence => ... } where needed

import scala.compiletime.summonFrom
import scala.concurrent.{Await, Future}
import scala.concurrent.duration.{*, given}
import scala.concurrent.ExecutionContext.Implicits.given

inline def getAll[T <: Tuple](futures: Tuple.Map[T, Future])(
  timeout: Long,
  units: TimeUnit
): T = inline futures match
  case _: EmptyTuple =>
    summonFrom {
      case _: (EmptyTuple =:= T) => EmptyTuple
    }
  case vfs: (fh *: ft) =>
    vfs match
      case vfh *: vft =>
        val start = System.currentTimeMillis()
        summonFrom {
          case _: (`fh` <:< Future[h]) =>
            val vh: h = Await.result(vfh, Duration(timeout, units))
            val elapsed = System.currentTimeMillis() - start
            val remaining = MILLISECONDS.convert(timeout, units) - elapsed

            summonFrom {
              case _: (Tuple.InverseMap[`ft`, Future] =:= t) =>
                summonFrom {
                  case _: (`ft` =:= Tuple.Map[`t` & Tuple, Future]) =>
                    summonFrom {
                      case _: ((`h` *: `t`) =:= T) =>
                        vh *: getAll[t & Tuple](vft)(remaining, units)
                    }
                }
            }
        }

Testing:

getAll[(Int, String)](Future(1), Future("a"))(5000, MILLISECONDS) // (1,a)

Maybe it's better to define getAll with Tuple.InverseMap (without Tuple.Map at all)

inline def getAll[T <: Tuple](futures: T)(
  timeout: Long,
  units: TimeUnit
): Tuple.InverseMap[T, Future] = inline futures match
  case _: EmptyTuple =>
    summonFrom {
      case _: (EmptyTuple =:= Tuple.InverseMap[T, Future]) => EmptyTuple
    }
  case vfs: (fh *: ft) =>
    vfs match
      case vfh *: vft =>
        val start = System.currentTimeMillis()
        summonFrom {
          case _: (`fh` <:< Future[h]) =>
            val vh: h = Await.result(vfh, Duration(timeout, units))
            val elapsed = System.currentTimeMillis() - start
            val remaining = MILLISECONDS.convert(timeout, units) - elapsed

            summonFrom {
              case _: ((`h` *: Tuple.InverseMap[`ft`, Future]) =:= (Tuple.InverseMap[T, Future])) =>
                vh *: getAll[ft](vft)(remaining, units)
            }
        }

Testing:

getAll(Future(1), Future("a"))(5000, MILLISECONDS) // (1,a)

Now you don't need to specify the type parameter of getAll at the call site.


Easier would be to define getAll recursively both on type level (math types) and value level (pattern matching). Then you don't need implicit hints

type GetAll[T <: Tuple] <: Tuple = T match
  case EmptyTuple => EmptyTuple
  case Future[h] *: ft => h *: GetAll[ft]

inline def getAll[T <: Tuple](futures: T)(
  timeout: Long,
  units: TimeUnit
): GetAll[T] = inline futures match
  case _: EmptyTuple => EmptyTuple
  case vfs: (Future[_] *: ft) =>
    vfs match
      case vfh *: vft =>
        val start = System.currentTimeMillis()
        val vh = Await.result(vfh, Duration(timeout, units))
        val elapsed = System.currentTimeMillis() - start
        val remaining = MILLISECONDS.convert(timeout, units) - elapsed

        vh *: getAll[ft](vft)(remaining, units)

Please notice that if you replace the recursive definition of GetAll with just

type GetAll[T <: Tuple] = Tuple.InverseMap[T, Future]

the code stops to compile. You'll have to add implicit hints again.


I'm reminding you the rules of match types:

This special mode of typing for match expressions is only used when the following conditions are met:

  1. The match expression patterns do not have guards
  2. The match expression scrutinee's type is a subtype of the match type scrutinee's type
  3. The match expression and the match type have the same number of cases
  4. The match expression patterns are all Typed Patterns, and these types are =:= to their corresponding type patterns in the match type

The compiler seems not to recognize the match-type definition accompanying a pattern-match definition if we specialize a type parameter along with introducing a type alias:

type A[T] = T match
  case Int    => Double
  case String => Boolean

def foo[T](t: T): A[T] = t match
  case _: Int    => 1.0
  case _: String => true

compiles and

type A[T] = T match
  case Int    => Double
  case String => Boolean

type B[T] = A[T]

def foo[T](t: T): B[T] = t match
  case _: Int    => 1.0
  case _: String => true

does and

type A[T, F[_]] = T match
  case Int    => Double
  case String => Boolean

def foo[T](t: T): A[T, Option] = t match
  case _: Int    => 1.0
  case _: String => true

does but

type A[T, F[_]] = T match
  case Int    => Double
  case String => Boolean

type B[T] = A[T, Option]

def foo[T](t: T): B[T] = t match
  case _: Int    => 1.0
  case _: String => true

doesn't (Scala 3.2.2). Also the order of cases is significant:

type A[T] = T match
  case Int    => Double
  case String => Boolean

def foo[T](t: T): A[T] = t match
  case _: String => true
  case _: Int    => 1.0

doesn't compile.

So the easiest implementation is

inline def getAll[T <: Tuple](futures: T)(
  timeout: Long,
  units: TimeUnit
): Tuple.InverseMap[T, Future] = inline futures match
  case vfs: (Future[_] *: ft) =>
    vfs match
      case vfh *: vft =>
        val start = System.currentTimeMillis()
        val vh = Await.result(vfh, Duration(timeout, units))
        val elapsed = System.currentTimeMillis() - start
        val remaining = MILLISECONDS.convert(timeout, units) - elapsed

        vh *: getAll[ft](vft)(remaining, units)

  case _: EmptyTuple => EmptyTuple

That's the order of cases as in the definition of Tuple.InverseMap https://github.com/lampepfl/dotty/blob/3.2.2/library/src/scala/Tuple.scala#L184-L187


See also

Scala 3: typed tuple zipping

Express function of arbitrary arity in vanilla Scala 3