简体   繁体   中英

Existential types and repeated parameters

Is it possible to have an existential type scope over the type of a repeated parameter in Scala?

Motivation

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.

Stuff I've tried

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.

Simplified example

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 simplified example

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

Answer to the first example

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!

Enhanced solution for the first example

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM