Scala ひと巡り : 明示的に型付けられた自己参照 (Explicitly Typed Self References)

原ページ

拡張可能なソフトウェアを開発するとき、値 this の型を宣言したくなることが時々あります。興味を引く例として、グラフデータ構造の小規模で拡張可能な表現を Scala で考えてみましょう。

次はグラフを記述する定義です:

abstract class Graph { type Edge type Node <: NodeIntf abstract class NodeIntf { def connectWith(node: Node): Edge } def nodes: List[Node] def edges: List[Edge] def addNode: Node }

グラフはノードとエッジのリストからなり、それらノードとエッジの両方の型とも抽象のままです。

The use of abstract types [21] allows implementations of trait Graph to provide their own concrete classes for nodes and edges.

抽象型 [21]を使えば、ノードとエッジに対してそれら自身の具象クラスを提供できるようにする、トレイト Graph を実装できます。

さらに、グラフに新しいノードを加える、メソッド addNode があります。ノードはメソッド connectWith を使って連結します。

次のプログラムは、クラス Graph の 1 つの考え得る実装です。

abstract class DirectedGraph extends Graph { type Edge <: EdgeImpl class EdgeImpl(origin: Node, dest: Node) { def from = origin def to = dest } class NodeImpl extends NodeIntf { 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 } }

クラス DirectedGraph は、部分的な実装を提供することで、Graph クラスを特化します。実装は本当に部分的です。なぜなら DirectedGraph をさらに拡張できるようにしたいからです。そのために、このクラスではすべての実装詳細をオープンにし、このようにノードとエッジの両方とも抽象のままに しています。にもかかわらず、クラス DirectedGraph は、クラス EdgeImpl へ境界を厳しくすることで、エッジ型の実装についてさらなる詳細を明示しています。また、クラス EdgeImpl と NodeImpl で表される、エッジとノードの準備的な実装がいくつかあります。部分的なグラフ実装の中で新しいノードとエッジオブジェクトを生成する必要があるので、 ファクトリメソッド newNode と newEdge も加えなければなりません。メソッド addNode と connectWith は共に、これらのファクトリメソッドを使って定義します。メソッド connectWith の実装をよく見ると、エッジを生成するために、自己参照 this をファクトリメソッド newEdge に渡す必要があることがわかります。しかし this は型 NodeImpl に割り当てられており、それは対応するファクトリメソッドによって要請される型 Node に互換ではありません。その結果、上記のプログラムは正しくなく、Scala コンパイラはエラーメッセージを発行します。

Scala では、自己参照 this に他の型を明示的に与えることで、クラスを(将来実装されるであろう)もう 1 つの型に結び付けることができます。このメカニズムを使って上記コードを修正できます。明示的な自己型は、クラス DirectedGraph の本体中で指定します。

次は修正したプログラムです:

abstract class DirectedGraph extends Graph { ... class NodeImpl extends NodeIntf { self: Node => def connectWith(node: Node): Edge = { val edge = newEdge(this, node) // 今度は OK edges = edge :: edges edge } } ... }

クラス NodeImpl の新しい定義では、this は型 Node を持っています。型 Node は抽象ですから、NodeImpl が本当に Node のサブ型かどうかはまだわからず、Scala の型システムは、このクラスのインスタンス化を許してくれません。

But nevertheless, we state with the explicit type_annotation of this that at some point, (a subclass of) NodeImpl has to denote a subtype of type Node in order to be instantiatable .

しかしそれにもかかわらず、インスタンス化できるようにするために、ある時点で NodeImpl (のサブクラスの 1 つ)が型 Node のサブ型を表さなければならないことを、this の明示的なアノテーション型を用いて記述します。

次は、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) }

この場合、NodeImpl をインスタンス化できます。なぜなら、今度は NodeImpl が(単純に NodeImpl のエイリアスである)型 Node のサブ型を表すことがわかるからです。

次は、クラス ConcreteDirectedGraph の使用方法を示す例です:

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) }