| [back to bqbase index] |
Existential Types in Scala |
Existential types provide a way of abstracting type information, such that (a) a provider can hide a concrete type ("pack"), and thus avoid any possibility of the client depending on it, and (b) a client can manipulate said type by only by giving it a name ("unpack") and making use of its bounds. Existentials play a big role in our understanding of abstract data types and encapsulation. However, they have not been taken up in mainstream programming languages so far. Recently, Martin proposed to add existentials to Scala (in order to make Scala's nice and clean generics operate smoothly with Java's not-so-nice and not-so-clean generics). Here, I won't go too much into Java interop. I am more interested in pattern matching. A small exampleIt's already high-time for an example. Take your typical generic OO types (I write T ::= C[T*] | X
A type is either a named type (with arguments) like
def headofany(x:Any) = x match {
case xs: List[a] => xs.head
// it has type a... but what is a?
}
This program says, match on When we don't know, we can always choose the easy way out and assign it type
def headof(x:Any) = x match {
case ys: List[a] => { x => ys }
// result is a function
// here, ys is List[a], what type has
// the function? Any => List[a]?
}
Here we take our
Still, Scala's definition-site variance comes to the rescue, and we can ignore existential types
in this example... since is covariant in its type parameter (it is defined as However, I wanted to talk about existentials. So, let's assume some type
def headof(x:Any) = x match {
case y: Foo[a] => { x => y }
// result is a function... what is its type?
// Any => Foo[a] wouldn't work
// because a is "free".
}
Here, there is no way to communicate that we tested what is it good for, again?Alas, existential types give you data abstraction, and data abstraction is a Good Thing. Enter a standard example for using existentials. In Cardelli and Wegner's words: "Hence, there are useful existential types which hide some of the structure of the objects they represent but show enough structure to allow manipulations of the objects through operations the objects themselves provide". In Scala, operations are typically grouped together in classes. Let me give another simplistic example.
class Archery[X,Y] {
def arrow: X;
def shoot(someArrow:X): Y
}
def foo(a:Any) = a match {
case arch:Archery[x,y] => arch.shoot(arch.arrow)
}
we can actually shoot our arrow without knowing of what type it is. Existentials, GADTs and ExtractorsWhat drives to write up all this stuff is that recently, we managed to provide Scala with a sort of user-definable pattern matching facility: the extractors. One thing we constantly kept in mind is that we would like the GADT examples to go throught, which are so en vogue in functional programming these days. It uses the dynamic type test done by the pattern match, like so:
class Term[X]
case class App[Y,Z](fun:Y=>Z, arg:Y)
extends Term[Z]
def foo[X](x:Term[X]) = x match {
case app: App[y,z] => ...
// ok, some y,z exists, you know the deal.
}
Now... what happens, if the App pattern is an extractors? That is, a user-defined method that
will test-and-deconstruct a
class Term[X]
object App {
// i'm hidden
case class InternalApply[Y,Z](fun:Y=>Z, arg:Y) extends Term[Z]
def apply[Y,Z](fun:Y=>Z, arg:Y): Term[Z] =
new InternalApply[Y,Z](fun,arg)
def unapply[Y,Z](app:InternalApply[Y,Z]) =
Some(app.fun, app.arg)
}
What we see here is a pretty convenient hack: the There's one thing where the perceived gain in extensibility (in contrast to having a case class Apply, you can change InternalApply at will) breaks down: If you compiled your program and then later decided, that With existentials, we could instead give a nicer type to the unapply, which does not even mention the internal thing in its signature
class Term[X]
object App {
// i'm hidden
case class InternalApply[Y,Z](fun:Y=>Z, arg:Y) extends Term[Z]
def apply[Y,Z](fun:Y=>Z, arg:Y): Term[Z] =
new InternalApply[Y,Z](fun,arg)
def unapply[X](arg:Term[X]): // existential return type!
Option[(Y=>Z,Y)] for_some {type Y, type Z} =
arg match {
case i:InternalApply[y,z] => Some(i.fun, i.arg)
case _ => None
}
}
This is a typical "pack" operation: we hide the types Y,Z. Actually, we hide them because we do not know them ourselves, since they resulted from an unpack operation %D, namely the pattern match. |
published:2007-06-13 last change:2007-09-19
changelog: