scalascala-3dottymatch-types

How to unpack the type of elements in tuple in dotty?


Suppose I have a class defined as:

case class Box[A](a: A)

I want to write a generic method that unpack tuple (Box[A1](a1), .., Box[An](an)) to a tuple (a1, .., an) with type (A1, .., An).

I tried with Match Types with no luck:

scala> type Unpack[Bs <: Tuple] <: Tuple = Bs match {
     |   case Unit => Unit
     |   case Box[a] *: bs => a *: Unpack[bs]
     | }

scala> def unpack[Bs <: Tuple](bs: Bs): Unpack[Bs] = bs match {
     |   case () => ()
     |   case Box(a) *: as => a *: unpack(as)
     | }
2 |  case () => ()
  |             ^^
  |Found:    Unit
  |Required: Unpack[Bs]
  |
  |where:    Bs is a type in method unpack with bounds >: Unit(?1) | Unit(?2) and <: Tuple
3 |  case Box(a) *: as => a *: unpack(as)
  |                       ^^^^^^^^^^^^^^^
  |Found:    A$1 *: Unpack[Tuple]
  |Required: Unpack[Bs]
  |
  |where:    Bs is a type in method unpack with bounds >: (Any *: Tuple)(?3) and <: Tuple

Solution

  • I am guessing that you tested code based on Concat example from the documentation. I tested this example... and it doesn't compile on 0.21.0-RC1.

    dotr -version
    Starting dotty REPL...
    Dotty compiler version 0.21.0-RC1 -- Copyright 2002-2019, LAMP/EPFL
    scala> type Concat[+Xs <: Tuple, +Ys <: Tuple] <: Tuple = Xs match {
         |   case Unit => Ys
         |   case x *: xs => x *: Concat[xs, Ys]
         | }
    1 |type Concat[+Xs <: Tuple, +Ys <: Tuple] <: Tuple = Xs match {
      |            ^^^^^^^^^^^^
      |covariant type parameter Xs occurs in invariant position in Xs match {
      |  case Unit => Ys
      |  case
      |    [x, xs <: Tuple] => scala.internal.MatchCase[x *: xs, x *: Concat[xs, Ys]]
      |} <: Tuple
    

    If I remove that + to make it similar to your example, type definition passes:

    type Concat[Xs <: Tuple, Ys <: Tuple] <: Tuple = Xs match {
         |   case Unit => Ys
         |   case x *: xs => x *: Concat[xs, Ys]
         | }
    
    scala>
    

    But I am unable to write an implementation:

    def concat[Xs <: Tuple, Ys <: Tuple](Xs: Xs, Ys: Ys): Concat[Xs, Ys] = Xs match {
         |   case () => Ys
         |   case x *: xs => x *: concat(xs, Ys)
         | }
    2 |  case () => Ys
      |             ^^
      |Found:    (Ys : Ys)
      |Required: Concat[Xs, Ys]
      |
      |where:    Xs is a type in method concat with bounds >: (?1 : Unit) | (?2 : Unit) and <: Tuple
      |          Ys is a type in method concat with bounds <: Tuple
    3 |  case x *: xs => x *: concat(xs, Ys)
      |                  ^^^^^^^^^^^^^^^^^^^
      |Found:    Any *: Concat[Tuple, Ys]
      |Required: Concat[Xs, Ys]
      |
      |where:    Xs is a type in method concat with bounds >: (?3 : Any *: Tuple) and <: Tuple
      |          Ys is a type in method concat with bounds <: Tuple
    
    
    

    So, let's consult the documentation. Thing is... currently there is not documentation how to implement things. There is a section telling us that things can be tricky.

    So how it looks in the actual code? Concat implementation in source code looks currently like this:

     def dynamicConcat[This <: Tuple, That <: Tuple](self: This, that: That): Concat[This, That] = {
        type Result = Concat[This, That]
    
        // If one of the tuples is empty, we can leave early
        (self: Any) match {
          case self: Unit => return that.asInstanceOf[Result]
          case _ =>
        }
    
        (that: Any) match {
          case that: Unit => return self.asInstanceOf[Result]
          case _ =>
        }
    
        val arr = new Array[Object](self.size + that.size)
    
        // Copies the tuple to an array, at the given offset
        inline def copyToArray[T <: Tuple](tuple: T, array: Array[Object], offset: Int): Unit = (tuple: Any) match {
          case xxl: TupleXXL =>
            System.arraycopy(xxl.elems, 0, array, offset, tuple.size)
          case _ =>
            tuple.asInstanceOf[Product].productIterator.asInstanceOf[Iterator[Object]]
              .copyToArray(array, offset, tuple.size)
        }
    
        // In the general case, we copy the two tuples to an array, and convert it back to a tuple
        copyToArray(self, arr, 0)
        copyToArray(that, arr, self.size)
        dynamicFromIArray[Result](arr.asInstanceOf[IArray[Object]])
      }
    

    Sure, probably someone could argue that is was for performance reasons(?), but it seems that (until some better way will be documented) values for match types can only be implemented in kind of hacky way using .asInstanceOf. And it is totally on you to make sure that values will match the deduced type (yuk!):

    scala> def unpack[Bs <: Tuple](bs: Bs): Unpack[Bs] = bs match {
         |   case () => ().asInstanceOf[Unpack[Bs]]
         |   case Box(a) *: as => (a *: unpack(as)).asInstanceOf[Unpack[Bs]]
         | }
    def unpack[Bs <: Tuple](bs: Bs): Unpack[Bs]
    
    scala> unpack( ( Box(1), Box("test") ) )
    val res0: Int *: Unpack[Box[String] *: Unit] = (1,test)
    
    scala>
    

    Hopefully, some Dotty contributor could suggest a better solution, but so far it is the only way I see it's doable.