Home > Blockchain >  Bind wildcard type argument in Scala
Bind wildcard type argument in Scala

Time:01-31

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:

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
  • Related