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—eg, 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
Note: I had a different, non-working, solution here before, but I edited it out.
Edit: version 4 now
sealed trait Rect2D[A] extends Product with Serializable { this: Inner[A] =>
val rows: Seq[Sized[Seq[A], N]] forSome { type N <: Nat }
def copy(rows: Seq[Sized[Seq[A], N]] forSome { type N <: Nat } = this.rows): Rect2D[A]
}
object Rect2D {
private[Rect2D] case class Inner[A](rows: Seq[Sized[Seq[A], N]] forSome { type N <: Nat }) extends Rect2D[A]
def apply[A, N <: Nat](rows: Sized[Seq[A], N]*): Rect2D[A] = Inner[A](rows)
def unapply[A](r2d: Rect2D[A]): Option[Seq[Sized[Seq[A], N]] forSome { type N <: Nat }] = Inner.unapply(r2d.asInstanceOf[Inner[A]])
}
Finally, a version that "works with case classes"! I'm sure most of that could be eliminated by macros, if I only knew how to use them.
(answer for the first example below)
It looks like you don't care about the precise type parameter of the X[_]
in case class Y(xs: X[_]*)
, as long as they are all the same. You just want to prevent users to create Y
s that don't respect this.
One way to achive this would be to make the default Y
constructor private:
case class Y private (xs: Seq[X[_]])
// ^^^^^^^ makes the default constructor private to Y, xs is still public
// Note also that xs is now a Seq, we will recover the repeated arg list below.
and define your own constructor this way:
object Y {
def apply[B](): Y = Y(Nil)
def apply[B](x0: X[B], xs: X[B]*): Y = Y(x0 +: xs)
// Note that this is equivalent to
// def apply[B](xs: X[B]*): Y = Y(xs)
// but the latter conflicts with the default (now private) constructor
}
Now one can write
Y()
Y(X("a"))
Y(X(1), X(1), X(5), X(6))
Y[Int](X(1), X(1), X(5), X(6))
and the following doesn't compile:
Y(X(1), X("1"))
We make the constructor private and change the repeated arg list to a Seq as above:
case class Rect2D[A] private (rows: Seq[Sized[Seq[A], _]])
// ^^^^^^^ ^^^^ ^
Let's define our own constructor(s):
object Rect2D {
def apply[A](): Rect2D[A] = Rect2D[A](Nil)
def apply[A,N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs)
}
Now the following compiles:
val r0: Rect2D[_] = Rect2D()
val r: Rect2D[Int] = Rect2D[Int]()
val r1: Rect2D[Int] = Rect2D(Sized[Seq](1, 2))
val r2: Rect2D[Int] = Rect2D(Sized[Seq](1, 2), Sized[Seq](2, 3))
val r3: Rect2D[Int] = Rect2D(Sized[Seq](1, 2), Sized[Seq](2, 3), Sized[Seq](2, 3), Sized[Seq](2, 3))
val r4: Rect2D[Any] = Rect2D(Sized[Seq](1, 2), Sized[Seq]("a", "b"), Sized[Seq](2, 3), Sized[Seq](2, 3)) // Works because both Sized and Seq are covariant
// Types added as a check, they can be removed
and the following doesn't:
val r5 = Rect2D(Sized[Seq](1, 2), Sized[Seq](1, 2, 3))
One drawback is that one cannot write something like
val r2 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq](2, 3))
// ^^^^^
one has to write this instead
val r2 = Rect2D[Int, Nat._2](Sized[Seq](1, 2), Sized[Seq](2, 3))
// ^^^^^^^^
Let's fix this!
A cleaner solution would be define the constructors above this way:
object Rect2D {
def apply[A,N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs) // Same as above
case class Rect2DBuilder[A]() {
def apply(): Rect2D[A] = Rect2D[A](Nil)
def apply[N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs)
}
def apply[A] = new Rect2DBuilder[A]
}
Now we can also write
val r2 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq](2, 3))
and the following would not compile
val r4 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq]("a", "b"), Sized[Seq](2, 3), Sized[Seq](2, 3))
// ^^^^^ ^^^^^^^^
Taking you simplified example: you could declare an additional type parameter on Y:
case class Y[V](xs: X[V]*)
This type parameter should be inferrable, so nothing extra to write from the user perspective.
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.