Is it possible to have an existential type scope over the type of a repeated parameter in Scala?
In this answer I use the following case class:
case class Rect2D[A, N <: Nat](rows: Sized[Seq[A], N]*)
It does what I want, but I don't care about N
(beyond needing to know that it's the same for all the rows), and would prefer not to have it in Rect2D
's type parameter list.
The following version gives me the wrong semantics:
case class Rect2D[A](rows: Sized[Seq[A], _ <: Nat]*)
The existential is under the *
, so I don't get the guarantee that all of the rows have the same second type parameter—e.g., the following compiles, but shouldn't:
Rect2D(Sized(1, 2, 3), Sized(1, 2))
The following version has the semantics I want:
case class Rect2D[A](rows: Seq[Sized[Seq[A], N]] forSome { type N <: Nat })
Here I'm using forSome
to lift the existential over the outer Seq
. It works, but I'd prefer not to have to write the Seq
in Rect2D(Seq(Sized(1, 2, 3), Sized(3, 4, 5)))
.
I've tried to do something similar with *
:
case class Rect2D[A](rows: Sized[Seq[A], N] forSome { type N <: Nat }*)
And:
case class Rect2D[A](rows: Sized[Seq[A], N]* forSome { type N <: Nat })
The first is (not surprisingly) identical to the _
version, and the second doesn't compile.
Consider the following:
case class X[A](a: A)
case class Y(xs: X[_]*)
I don't want Y(X(1), X("1"))
to compile. It does. I know I can write either:
case class Y(xs: Seq[X[B]] forSome { type B })
Or:
case class Y[B](xs: X[B]*)
But I'd like to use repeated parameters and don't want to parametrize Y
on B
.
In case this does not violate your contract, since you do not care about N, you can exploit covariance to throw the existential type away like the following:
trait Nat
trait Sized[A,+B<:Nat]
object Sized {
def apply[A,B<:Nat](natSomething:B,items: A *) = new Sized[Seq[A],B] {}
}
class NatImpl extends Nat
case class Rect2D[A](rows:Sized[Seq[A],Nat] * )
val sizedExample = Sized(new NatImpl,1,2,3)
Rect2D(Sized(new NatImpl,1,2,3),Sized(new NatImpl,1,2,3),Sized(new NatImpl,1,2,3))
The idea here is that you do not care about capturing the second generic parameter of the Sized[A,B] because you do not use it. So you make class covariant in B, that means that
Sized[A,B] <:< Sized[A,C]
if B<:<C
The problem with the existential type is that you are requiring it to be the same for all the objects passed to the constructor of Rect2D, but clearly this is not possible because its an existential type, so the compiler cannot verify it.
If you cannot make it covariant but controvariant, the same approach will work: you make the class controvariant in B:
Sized[A,B] <:< Sized[A,C]
if C<:<B
then you can exploit the fact Nothing is a subclass of everything:
trait Nat
trait Sized[A,-B<:Nat]
object Sized {
def apply[A,B<:Nat](natSomething:B,items: A *) = new Sized[Seq[A],B] {}
}
class NatImpl extends Nat
case class Rect2D[A](rows:Sized[Seq[A],Nothing] * )
val sizedExample = Sized(new NatImpl,1,2,3)
Rect2D(Sized(new NatImpl,1,2,3),Sized(new NatImpl,1,2,3),Sized(new NatImpl,1,2,3))
The reason why you cannot use an existential parameter to verify all rows have the same second type is because the _ does not mean "a type" but "an unknown type"
Seq[Seq[_]]
for example means a Seq where every element is of type Seq[_] , but since _ is unknown, there is no possibility to verify each seq has the same type.
If your class does not have to be a case class, the best solution in terms of elegance would be to use either the variance/controvariance approach with a private constructor, with two generics parameters, A and N
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With