Scalaではクラスが他のクラスをメンバーとして保持することが可能です。 Javaのような、内部クラスが外側のクラスのメンバーとなる言語とは対照的に、Scalaでは、内部クラスは外側のオブジェクトに束縛されます。 どのノードがどのグラフに属しているのかを私達が混同しないように、コンパイラがコンパイル時に防いでほしいのです。 パス依存型はその解決策の1つです。
その違いを示すために、グラフデータ型の実装をさっと書きます。
class Graph {
class Node {
var connectedNodes: List[Node] = Nil
def connectTo(node: Node): Unit = {
if (!connectedNodes.exists(node.equals)) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}
このプログラムはグラフをノードのリスト(List[Node]
)で表現しています。いずれのノードも接続している他のノードへのリスト(connectedNodes
)を保持します。class Node
は class Graph
の中にネストしているので、 パス依存型 です。
そのためconnectedNodes
の中にある全てのノードは同じGraph
インスタンスからnewNode
を使用して作る必要があります。
val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
val node3: graph1.Node = graph1.newNode
node1.connectTo(node2)
node3.connectTo(node1)
わかりやすくするためnode1
、node2
、node3
の型をgraph1.Node
と明示的に宣言しましたが、なくてもコンパイラは推論できます。
これはnew Node
を呼んでいるgraph1.newNode
を呼び出す時、メソッドがNode
のインスタンスgraph1
を使用しているからです。
2つのグラフがあるとき、Scalaの型システムは1つのグラフの中で定義されたノードと別のグラフで定義されたノードを混ぜることを許しません。 それは別のグラフのノードは別の型を持つからです。 こちらは不正なプログラムです。
val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
node1.connectTo(node2) // legal
val graph2: Graph = new Graph
val node3: graph2.Node = graph2.newNode
node1.connectTo(node3) // illegal!
型graph1.Node
はgraph2.Node
とは異なります。Javaであれば先のプログラム例の最後の行は正しいでしょう。
2つのグラフのノードに対して、Javaは同じ型Graph.Node
を指定します。つまりクラスGraph
はNode
の接頭辞です。
Scalaではそのような型も同様に表現することができ、Graph#Node
と書きます。
もし他のグラフのノードに接続できるようにしたければ、以下の方法で最初のグラフ実装の定義を変える必要があります。
class Graph {
class Node {
var connectedNodes: List[Graph#Node] = Nil
def connectTo(node: Graph#Node): Unit = {
if (!connectedNodes.exists(node.equals)) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}