In Scala 2, you can of course use use wildcard or existential types as type arguments. However, this means that you do not always have a name for a type you'd like to use. This sometimes leads to odd situations where you need to lean on type inference to circumvent writing a type explicitly.
Here is a somewhat contrived example of what I mean:
case class Container[T](value: T) {
def replace(value: T): Container[T] = Container(value)
}
def identity[T](container: Container[T]): Container[T] = {
// A weird way of writing the identity function,
// but notice that I have essentially given the name
// `T` to the `_`
container.replace(container.value)
}
var x: Container[_] = Container[Int](1)
// This works, but as far as I know, there's no way to explicitly
// pass the type for `T`. For example, something like identity[_](x) won't work.
identity(x)
// This also fails to work, but if we could assign a name to the `_`, like `T`,
// then it would become obvious that this should work.
// x.replace(x.value)
Is there a way to get around this more cleanly? It would be really great if you could write something like:
let Container[T] = x.type in {
// Now there is a type T in this scope,
// and x has type `Container[T]`
}
As far as I'm aware, nothing of the sort exists in Scala. Is there a feature I'm missing. Also, does anyone know of similar features in other languages?
CodePudding user response:
Use type pattern matching (tested with 2.13):
case class Container[T](value: T) {
def replace(value: T): Container[T] = Container(value)
}
val x: Container[_] = Container[Int](1)
val y: Container[_] = x match {
case c => c.replace(c.value)
}
The actual type itself does not have a name in code, and isn't actually visible, but what's basically happening is this:
case class Container[T](value: T) {
def replace(value: T): Container[T] = Container(value)
}
val x: Container[_] = Container[Int](1)
val y: Container[_] = x match {
case c: Container[t] =>{
val v: t = c.value
c.replace(v)
}
}
The type pattern t
binds the existentially quantified type, and can be used in subsequent expressions, so that v: t
can be typed, and c.replace(v)
is also properly typed.
See also the following related questions:
- Mapping over a list of existential types
- Why does this snippet with pattern matching and higher kinded types no longer compile in Scala 2.12?
CodePudding user response:
One more option is to make T
a type member rather than type parameter. In such case the existential type corresponds to just Container
while a specific type is Container { type T = ... }
(aka Container.Aux[...]
)
trait Container {
type T
def value: T
def replace(value: T): Container.Aux[T] = Container(value)
}
object Container {
type Aux[_T] = Container { type T = _T }
def apply[_T](_value: _T): Aux[_T] = new Container {
override type T = _T
override val value: T = _value
}
}
val x: Container = Container[Int](1)
x.replace(x.value) // compiles
def identity[T](container: Container.Aux[T]): Container.Aux[T] =
container.replace(container.value)
identity[x.T](x) // compiles
Please notice that I made x
a val
rather than var
so that the path-dependent type x.T
makes sense.
Maybe you prefer to keep a case class because of all the syntax sugar the compiler generates for case classes. In such case we could introduce an additional trait
trait IContainer {
type T
def value: T
def replace(value: T): IContainer.Aux[T]
}
object IContainer {
type Aux[_T] = IContainer { type T = _T }
}
case class Container[_T](value: _T) extends IContainer {
override type T = _T
override def replace(value: T): Container[T] = Container(value)
}
val x: IContainer = Container[Int](1)
x.replace(x.value) // compiles
def identity[T](container: IContainer.Aux[T]): IContainer.Aux[T] =
container.replace(container.value)
identity[x.T](x) // compiles