Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Existential types and repeated parameters

Tags:

types

scala

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—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.

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.

like image 604
Travis Brown Avatar asked Jul 18 '12 11:07

Travis Brown


1 Answers

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

like image 70
Edmondo1984 Avatar answered Oct 01 '22 05:10

Edmondo1984