We start by discussing parametrized types again, and then we will contrast them with the new concept of abstract types. These two features partially overlap. Nevertheless, each feature is better adjusted to different design problems. Parametrized types are the best for the definition of collections; abstract types are the best for the definition of type families and other complex type scenarios.
In the bibliographic reference [D], the abstraction corresponding to the parametrized types is called functional abstraction, and the abstraction corresponding to abstract types is called object-oriented abstraction.
abstract class AbsCell[T] { val init: T // Abstract value private var value: T = init def get: T = value def set(x: T): Unit = { value = x } }We can instantiate the parametrized class to produce another, more specific, class:
class IntCell extends AbsCell[Int] { val init = 0 }Here is an example of creation:
val cell = new IntCell scala> cell res2: IntCell = IntCell@1578426We can also directly generate a compatible singleton:
val cell = new AbsCell[Int] { val init = 0 } scala> cell cell: AbsCell[Int] = $anon$1@191c263
Using abstract types, the generic cell type is defined as follows:
abstract class AbsCell { type T // Abstract type val init: T // Abstract value private var value: T = init def get: T = value def set(x: T): Unit = { value = x } }We can instantiate the parametrized class to produce another, more specific class:
class IntCell extends AbsCell { type T = Int ; val init = 0 }Here is an example of creation:
val cell = new IntCell scala> cell res2: IntCell = IntCell@1578426We can also directly generate a compatible singleton:
val cell = new AbsCell { type T = Int ; val init = 0 } scala> cell cell: AbsCell{type T = Int} = $anon$1@ed7f5cIn the previous type we say that the type AbsCell was augmented with the refinement {type T = Int}.
For proper support of abstract types, Scala needs to add some new mechanism that are discussed next.
def reset(c: AbsCell): Unit = { c.set(c.init) }This function is safe because c.init has type c.T and c.set has type (c.T)Unit.
c.T is one example of a path-dependent type. In general, such a type has the form x1.x2.x3. ... .xn.T, where the prefix x1.x2.x3. ... .xn is a sequence of immutable values and T is a type member of xn.
Here is an abstract definition of graphs. The use of abstract types allows implementations of the abstract class to provide their own concrete classes for nodes and edges.
trait Graph { type Edge type Node <: NodeAbstr abstract class NodeAbstr { def connectWith(node: Node): Edge } def nodes: List[Node] def edges: List[Edge] def addNode: Node // creates a new node internally, and returns it }We want to use our graphs as in the following example:
object GraphTest extends Application { val g: Graph = new ConcreteDirectedGraph val n1 = g.addNode val n2 = g.addNode val n3 = g.addNode n1.connectWith(n2) n2.connectWith(n3) n1.connectWith(n3) }Now, here is a concrete implementation of directed graphs. The protected methods newNode and newEdge were introduced to allow further refinements in subclasses, as the the types Edge and Node are already frozen.
class ConcreteDirectedGraph extends Graph { type Edge = EdgeImpl type Node = NodeImpl class EdgeImpl(origin: Node, dest: Node) { def from = origin def to = dest } class NodeImpl extends NodeAbstr { def connectWith(node: Node): Edge = { val edge = newEdge(this, node) edges = edge :: edges edge } } protected def newNode: Node = new NodeImpl protected def newEdge(f: Node, t: Node): Edge = new EdgeImpl(f, t) var nodes: List[Node] = Nil var edges: List[Edge] = Nil def addNode: Node = { val node = newNode nodes = node :: nodes node } }Now, we try to develop an alternative, less concrete implementation of directed graphs. The following abstract class commits itself with many implementation details, however it still leaves some implementation details open to the point that both the edge and the node type are left abstract.
abstract class DirectedGraph extends Graph { type Edge <: EdgeImpl class EdgeImpl(origin: Node, dest: Node) { def from = origin def to = dest } class NodeImpl extends NodeAbstr { def connectWith(node: Node): Edge = { val edge = newEdge(this, node) edges = edge :: edges edge } } protected def newNode: Node protected def newEdge(from: Node, to: Node): Edge var nodes: List[Node] = Nil var edges: List[Edge] = Nil def addNode: Node = { val node = newNode nodes = node :: nodes node } }But this code is invalid. The following compiler error is issued:
<console>:13: error: type mismatch; found : DirectedGraph.this.NodeImpl required: DirectedGraph.this.Node val edge = newEdge(this, node) ^This problem is that newEdge is expecting a Node at the first position but is called with a NodeImpl instead... and NodeImpl is not a subtype of Node.
The solution if to insert this: Node => at the beginning of the class NodeImpl to obtain:
class NodeImpl extends NodeAbstr { this: Node => def connectWith(node: Node): Edge = { val edge = newEdge(this, node) edges = edge :: edges edge } }Now, inside the class NodeImpl, this has type Node. With the explicitly typed self reference we state that at some point, the class NodeImpl (or a subclass) has to denote a subtype of Node in order to be instantiatable.
Now, here is the simplest fully concrete implementation of the abstract class DirectedGraph:
class ConcreteDirectedGraph extends DirectedGraph { type Edge = EdgeImpl type Node = NodeImpl protected def newNode: Node = new NodeImpl protected def newEdge(f: Node, t: Node): Edge = new EdgeImpl(f, t) }
"Self-types can be arbitrary; they need not have a relation with the class being defined. Type soundness is still guaranteed, because of two requirements: (1) the self-type of a class must be a subtype of the self-types of all its base classes, (2) when instantiating a class in a new expression, it is checked that the self type of the class is a supertype of the type of the object being created."
The object-oriented abstraction mechanism of Scala provides an excellent solution for an usually hard to implement concept of family polymorphism. A program exhibits family polymorphism when there is a family of types that vary together covariantly, along parallel hierarchies.
The following example is adapted from the Scala documentation.
We are going to discuss an implementation in Scala of the publish/subscribe design pattern, the same design pattern that is used is JavaBeans and the Swing library of Java.
Subject: Define a method subscribe that a observer use to register on a particular subject. Also define method publish to notify the observers of a subject of an event, topically when the internal state of the subject changes.
Observer: Define a method notify that is used by the subjects to notify the observer.
A subscribe method takes the registering observer as parameter, whereas an notify method takes the subject that did the notification as parameter. Hence, subjects and observers refer to each other in their method signatures. All elements of this design pattern are captured in the following system.
The definition of the pattern abstract on the types of the subjects and the observers, to allow covariant extensions of these classes in client code. Why is an explicitly typed self reference needed here?
trait SubjectObserver { type S <: Subject type O <: Observer abstract class Subject { this: S => private var observers: List[O] = List() def subscribe(obs: O) = observers = obs :: observers def publish = for (obs <- observers) obs.notify(this) } trait Observer { def notify(sub: S): Unit } }Now a concrete instance of the design pattern.
object SensorReader extends SubjectObserver { type S = Sensor type O = Display abstract class Sensor extends Subject { val label: String var value: Double = 0.0 def changeValue(v: Double) = { value = v publish } } class Display extends Observer { def notify(sub: Sensor) = println(sub.label + " has value " + sub.value) } }Finally, one example of use:
object Test { import SensorReader._ val s1 = new Sensor { val label = "sensor1" } val s2 = new Sensor { val label = "sensor2" } def main(args: Array[String]) = { val d1 = new Display val d2 = new Display s1.subscribe(d1); s1.subscribe(d2) s2.subscribe(d1) s1.changeValue(2); s2.changeValue(3) } }