I follow what I see in the implementation of standard library.
The idea is that we have a clean interface that is typed and a runtime implementation of that interface that is efficient.
So, first of all, I need to encode the information of ZipAll
inside a type:
type ZipAll [L <: Tuple, R <: Tuple, TL, TR] <: Tuple = (L, R) match {
case (hl *: restl, hr *: restr) => (hl, hr) *: ZipAll[restl, restr, TL, TR]
case (EmptyTuple, h *: rest) => (TL, h) *: ZipAll[EmptyTuple, rest, TL, TR]
case (h *: rest, EmptyTuple) => (h, TR) *: ZipAll[rest, EmptyTuple, TL, TR]
case (EmptyTuple, EmptyTuple) => EmptyTuple
}
This particular type ensures that when we will use ZipAll
, the corresponding resultant type will be correct. In particular, ZipAll
creates a tuple filled with the missing element using a standard type TL (if the left tuple is shorter than the right one) or TR (viceversa). Hence, all the next type tests are correct:
summon[ZipAll[(Int, Int), (Int, Double, Int), String, Int] =:= ((Int, Int), (Int, Double), (String, Int))
summon[ZipAll[(Int, Double, Int), (Int, Int), String, Int] =:= ((Int, Int), ( Double, Int), (Int), Int)
Now, we can create the type signature of our implementation (using the extension method):
extension[L <: Tuple](t: L) {
def zipAll[R <: Tuple, TR, TL](
r: R, endL: TL, endR: TR
): ZipAll[L, R, TL, TR] = ???
}
Now, we can create an unsafe implementation of zipAll
:
def runtimeZipAll(l: Tuple, r: Tuple, endL: Any, endR: Any): Tuple = (l, r) match {
case (hl *: restl, hr *: restr) => (hl, hr) *: zipAll(restl, restr, endL, endR)
case (EmptyTuple, hr *: restr) => (endL, hr) *: zipAll(EmptyTuple, restr, endL, endR)
case (hl *: restl, EmptyTuple) => (hl, endR) *: zipAll(restl, EmptyTuple, endL, endR)
case (EmptyTuple, EmptyTuple) => EmptyTuple
}
This should be hidden inside the implementation. Then, we can enforce the correct type of that unsafe implementation using asInstanceOf...
:
def zipAll[R <: Tuple, TR, TL](r: R, endL: TL, endR: TR): ZipAll[L, R, TL, TR] =
runtimeZipAll(l, r, endL, endR).asInstanceOf[ZipAll[L, R, TL, TR]]
Then, I should use the new functionality seamlessly:
val res: ((Int, Int), (Int, Int), (String, Int)) = (1, 2).zipAll((10, 20, 30), "hello", 20.0) // should be correctly typed
Probably it is not the best implementation so far (indeed the internal details are not type-safe), but it is kind of a pattern to implement safe interface with unsafe implementation (to achieve a good balance between type safety and performance).
Here the implementation of what I have describes so far in Scastie.