Search code examples
scalagenericstypestuples

Scala 3 generic tuples: type bound & converting to Seq


I have two problems, which I think are related.

I want to have a type for tuples, of which all elements of the tuple are at least an Animal, in Scala 3.3.1. I came up with the following:

// In ReproDef.scala
object ReproDef {
  type SubtypeTuple[T, L <: Tuple] <: Tuple = L match {
    case EmptyTuple => EmptyTuple
    case head *: tail => AsSubtype[T, head]#Out *: SubtypeTuple[T, tail]
  }

  class AsSubtype[T, U] {
    type Out = U
  }

  given [T, U <: T]: AsSubtype[T, U] = new AsSubtype[T, U]

  sealed trait Animal
  case class Cat() extends Animal
  case class Dog() extends Animal
  case class Bird() extends Animal

  val validTuple: SubtypeTuple[Cat | Dog | Bird, (Cat, Dog, Bird)] = Cat() *: Dog() *: Bird() *: EmptyTuple
  val validTuple2: SubtypeTuple[Animal, (Animal, Animal)] = (Cat(), Dog())
  val validTuple3: (Animal, Animal) = validTuple2

  def toSeq[L <: Tuple](tup: SubtypeTuple[Animal, L]): Seq[Animal] = {
    tup match {
      case empty: EmptyTuple => Seq.empty
      case (head: Animal) *: (tail: SubtypeTuple[Animal, tail_]) => head +: toSeq(tail)
    }
  }
}

(Full disclosure: I had a different solution, which worked, but was less readable. The above code is generated with ChatGPT, but should do the same, afaict. At least the errors I get are identical.)

Now here comes my first question: is there a better way to formulate this concept of a tuple where each element is at least a certain type T?

My second question is that I want to be able to convert tuples containing only animals to Sequences of Animals. The Tuple type contains a toList method, but that works only sometimes:

// In ReproUse.scala
def foo(): Unit = {
  val sig: (ReproDef.Cat, ReproDef.Dog) = (ReproDef.Cat(), ReproDef.Dog())
  def myId(x: Tuple): Tuple = x
  val x: Seq[ReproDef.Animal] = myId(sig).toList // Compiler error, see below
  println(myId(sig).toList) // Works, also without myId of course
}

Compiler error:

Found:    List[Tuple.Union[(?1 : Tuple)]]
Required: Seq[testdyn.ReproDef.Animal]

where:    ?1 is an unknown value of type Tuple


Note: a match type could not be fully reduced:

  trying to reduce  Tuple.Union[(?1 : Tuple)]
  trying to reduce  scala.Tuple.Fold[(?1 : Tuple), Nothing, [x, y] =>> x | y]
  failed since selector  (?1 : Tuple)
  does not match  case EmptyTuple => Nothing
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case h *: t => h | scala.Tuple.Fold[t, Nothing, [x, y] =>> x | y] [7:35]

In hopes of avoiding this typecheck error, I also defined my own toSeq, see the very first code snippet. However, that also gives a type error:

// In ReproUse.scala
def bar(): Unit = {
  val sig: (ReproDef.Cat, ReproDef.Dog) = (ReproDef.Cat(), ReproDef.Dog())
  println(ReproDef.toSeq(sig))
}

Error:

Found:    (sig : (testdyn.ReproDef.Cat, testdyn.ReproDef.Dog))
Required: testdyn.ReproDef.SubtypeTuple[testdyn.ReproDef.Animal, L]

where:    L is a type variable with constraint <: Tuple


Note: a match type could not be fully reduced:

  trying to reduce  testdyn.ReproDef.SubtypeTuple[testdyn.ReproDef.Animal, L]
  failed since selector  L
  does not match  case EmptyTuple => EmptyTuple
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case head *: tail => testdyn.ReproDef.AsSubtype[testdyn.ReproDef.Animal, head]#Out *:
  testdyn.ReproDef.SubtypeTuple[testdyn.ReproDef.Animal, tail] [14:28]

My custom toSeq method does work in the file ReproDef.scala, the problem starts occurring only when I want to use that method outside the file.

I think I am simply getting confused where the boundary is for typechecking, and I am probably expecting too much. Can someone explain to me my oversight, and what kind of type annotations I have to add to get these small examples to compile? Or is there maybe a better match type I can use that Scala 3 can work with better? The main motivaiton for this question is that I will probably use this toSeq method quite a few times, so I'd like it to cause as few type annotations as possible.


Solution

  • A1: Is this one line solution enough? (test it on line)

    type SubtypeTuple[E >: Tuple.Union[T], T <: Tuple] = T
    

    A2: You should change

    def foo(): Unit = {
      val sig: (ReproDef.Cat, ReproDef.Dog) = (ReproDef.Cat(), ReproDef.Dog())
      def myId(x: Tuple): Tuple = x
      val x: Seq[ReproDef.Animal] = myId(sig).toList // Compiler error, see below
      println(myId(sig).toList) // Works, also without myId of course
    }
    

    to

    //...
      def myId[T <: Tuple](x: T): T = x
    //...
    

    Since your method droped the type information of T.

    Edit According to your comments, I think you need typeclass for type constraint instead. Something like:

    scala> trait Animals[T]:
         |     def values(animals: T): Seq[Animal]
         |
         | object Animals:
         |     given Animals[EmptyTuple] = animals => Nil
         |     given [H <: Animal, T <: Tuple : Animals]: Animals[H *: T] =
         |         case h *: tail => h +: summon[Animals[T]].values(tail)
         |
    
    scala> class Zoo[T: Animals](animals: T):
         |     def toSeq: Seq[Animal] = summon[Animals[T]].values(animals)
         |
    // defined class Zoo
    
    scala> Zoo((Dog(), Cat(), Bird())).toSeq
    val res0: Seq[Animal] = List(Dog(), Cat(), Bird())