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:
- The match expression patterns do not have guards
- The match expression scrutinee's type is a subtype of the match type scrutinee's type
- The match expression and the match type have the same number of cases
- 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