I'm trying to zip tuples together and use match types to get the exact type of the resulting zip. I have a match type and the function:
type Z[A <: Tuple, B <: Tuple] <: Tuple = (A, B) match
case (EmptyTuple, EmptyTuple) => EmptyTuple
case (a *: as, b *: bs) => (a, b) *: Z[as, bs]
def z[A <: Tuple, B <: Tuple](a: A, b: B): Z[A, B] = (a, b) match
case (EmptyTuple, EmptyTuple) => EmptyTuple
case (ah *: at, bh *: bt) => (ah, bh) *: z(at, bt)
However, both cases in z()
result in an error:
Found: EmptyTuple.type Required: test.Tuples.Z[A, B]
and Found: (Any, Any) *: test.Tuples.Z[Tuple, Tuple] Required: test.Tuples.Z[A, B]
, respectively. I would have guessed this would be a pretty straightforward application of match types, but clearly I'm wrong. What am I missing here?
I would also like to restrict both the match type and the function z()
to tuples that have the same length (like how shapeless' Length
could be used in scala 2), but perhaps that is a separate question.
EDIT:
I managed to get the function z()
working with explicit casting, but I still think there has to be a way to avoid that:
def z[A <: Tuple, B <: Tuple, M <: Int](a: A, b: B): Z[A, B] = (a, b) match
case (ah *: at, bh *: bt) => ((ah, bh) *: z(at, bt)).asInstanceOf[Z[A, B]]
case (EmptyTuple, EmptyTuple) => EmptyTuple.asInstanceOf[Z[A, B]]
Also, I was able to get the length aspect working for the function z()
as well, but I would love to know if a) there is a cleaner/less verbose way to achieve this (perhaps without the need to define L
) and b) if there's a way to restrict the type arguments to Z
to be tuples of the same length:
type L[T <: Tuple] <: Int = T match
case EmptyTuple => 0
case _ *: t => 1 + L[t]
type Z[A <: Tuple, B <: Tuple] <: Tuple = (A, B) match
case (EmptyTuple, EmptyTuple) => EmptyTuple
case (a *: as, b *: bs) => (a, b) *: Z[as, bs]
def z[A <: Tuple, B <: Tuple, M <: Int](a: A, b: B)(using L[A] =:= L[B]): Z[A, B] = (a, b) match
case (ah *: at, bh *: bt) => ((ah, bh) *: z(at, bt)).asInstanceOf[Z[A, B]]
case (EmptyTuple, EmptyTuple) => EmptyTuple.asInstanceOf[Z[A, B]]
println(z(1 *: true *: EmptyTuple, "seven" *: 9.8 *: EmptyTuple)) // <-- correctly zips tuples: ((1,seven),(true,9.8))
// println(z(1 *: EmptyTuple, "seven" *: 9.8 *: EmptyTuple)) // <-- results in compile-time error as desired: "Cannot prove that test.Tuples.L[(Int *: EmptyTuple.type)] =:= test.Tuples.L[(String, Double)]..."
EDIT 2:
So actually it turns out that the tuple lengths are already restricted to be equal, as otherwise the match type Z
does not resolve, so I guess the question is just how to avoid the casts in z()
.
Currently match types have many limitations on value level:
Your code
breaks condition 4.
case (EmptyTuple, EmptyTuple)
andcase (ah *: at, bh *: bt)
are not typed patterns.It would make sense to try
but unfortunately this doesn't work because of type erasure
Actually,
case _: (EmptyTuple, EmptyTuple)
andcase t: (_ *: _, _ *: _)
are justcase _: (_, _)
and match everything. And if we swap the cases thenz((1, "a"), (true, 2.0))
throws runtime exception (java.lang.IndexOutOfBoundsException: 0
).The following approach with nested match types/pattern matchings seems to work
How to get match type to work correctly in Scala 3
Shapeless3 and annotations