简体   繁体   中英

Is it possible to write a "zipAll" function for scala inductive list (shapeless.HList in scala 2 or Tuple in scala 3)?

To this date, the "zip" function in HList works like zip function or untyped List in scala. It will discard elements from the longer operand to be conformant to the shorter element:

val a = 1 :: 2 :: 3 :: HNil
val b = 4 :: 5 :: HNil

println(a.zip(b))
// this results in (1, 4) :: (2, 5) :: HNil

There is no operation for HList equivalent or similar to "zipAll" function, which takes 2 extra arguments denoting the placeholder for each operand if it is too short to be zipped with the other.

Is it possible to do it for shapeless.HList or scala 3 Tuple? What's the shorted way to implement this?

Thanks a lot for your opinion.

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 shorted then 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM