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.
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())