Scala ひと巡り : sealed クラス (Sealed Classes)
sealed クラスは、継承するテンプレートを継承されるクラスと同じソースファイル中で定義する場合を除き、直接には継承できません。しかし、sealed クラスのサブクラスはどこででも継承できます。
sealed クラスは sealed 修飾子を使って定義できます。
もしパターンマッチのセレクタが sealed クラスのインスタンスなら、パターンマッチングのコンパイル時に、与えられたパターンセットが網羅的ではないと診断する警告が出ます。すなわち、実行時に MatchError
[60] が発生する可能性があるということです。
@unchecked アノテーション
[47] をマッチ式のセレクタに適用すると、そうでなければ発せられるはずの、非網羅的パターンマッチに関するあらゆる警告が抑制されます。
例えば、次のメソッド定義に対しては警告はありません。
def f(x: Option[Int]) = (x: @unchecked) match {
case Some(y) => y
}
@unchecked アノテーションがなければ、Scala コンパイラはパターンマッチが非網羅的であると推論し、Optionが sealed クラスなので警告を発します。
Scala ひと巡り : トレイト (Traits)
トレイトは、Java のインターフェースに似て、サポートするメソッドのシグニチャを記述することで、オブジェクトの型定義に使えます。Java と異なり、Scala のトレイトは部分的に実装できます。すなわち、複数のメソッドのデフォルト実装を定義できます。クラス
[2]と異なり、トレイトはコンストラクタ・パラメータを持てません。
次は 1 つの例です:
trait Similarity {
def isSimilar(x: Any): Boolean
def isNotSimilar(x: Any): Boolean = !isSimilar(x)
}
このトレイトは 2 つのメソッド isSimilar と isNotSimilar からなります。isSimilar が具象の(具体的な)メソッド実装を提供せず(Java 用語では抽象)、メソッド isNotSimilar は具象実装を定義します。従って、このトレイトを統合するクラスだけが、isSimilar に具象の実装を提供する必要があります。isNotSimilar の振る舞いは、トレイトから直接に継承します。トレイトは通常、ミックスインクラス合成
[5]を用いて、クラス
[61](あるいは他のトレイト)に統合されます:
class Point(xc: Int, yc: Int) extends Similarity {
var x: Int = xc
var y: Int = yc
def isSimilar(obj: Any) =
obj.isInstanceOf[Point] &&
obj.asInstanceOf[Point].x == x
}
object TraitsTest extends Application {
val p1 = new Point(2, 3)
val p2 = new Point(2, 4)
val p3 = new Point(3, 3)
println(p1.isNotSimilar(p2))
println(p1.isNotSimilar(p3))
println(p1.isNotSimilar(2))
}
次はプログラムの出力です:
false
true
true
Scala ひと巡り : 上限 型境界 (Upper Type Bounds)
Scala では、型パラメータ
[16]と抽象型
[21]は、型境界による制約を受けます。そのような型境界は、型変数の具象値を制限し、型のメンバーについてさらに多くの情報を明らかにします。上限型境界 T <: A は、型変数 T が型 A のサブ型を参照することを宣言します。
次は、多相的メソッド
[25] findSimilar の実装について、上限型境界に頼る例です:
trait Similar {
def isSimilar(x: Any): Boolean
}
case class MyInt(x: Int) extends Similar {
def isSimilar(m: Any): Boolean =
m.isInstanceOf[MyInt] &&
m.asInstanceOf[MyInt].x == x
}
object UpperBoundTest extends Application {
def findSimilar[T <: Similar](e: T, xs: List[T]): Boolean =
if (xs.isEmpty) false
else if (e.isSimilar(xs.head)) true
else findSimilar[T](e, xs.tail)
val list: List[MyInt] = List(MyInt(1), MyInt(2), MyInt(3))
println(findSimilar[MyInt](MyInt(4), list))
println(findSimilar[MyInt](MyInt(2), list))
}
上限型境界アノテーションがなければ、メソッド findSimilar 中でメソッド isSimilar を呼び出すことはできません。
Scala ひと巡り : 下限 型境界 (Lower Type Bounds)
上限型境界
[18]は、型を他の型のサブ型へ制限し、他方、下限型境界は、型が他の型のスーパー型であると宣言します。項 T >: A は、型パラメータ T あるいは抽象型 T が、型 A のスーパー型を参照することを表します。
次はそれが役に立つ例です:
case class ListNode[T](h: T, t: ListNode[T]) {
def head: T = h
def tail: ListNode[T] = t
def prepend(elem: T): ListNode[T] =
ListNode(elem, this)
}
上記のプログラムは prepend (先頭に追加)操作を使って連結リストを実装します。残念ながら、この型は、クラス ListNode の型パラメータ中にあって、非変です; すなわち、型 ListNode[String]は 型 List[Object]のサブ型ではありません。変位指定アノテーション
[17]の助けを借りて、このようなサブ型のセマンティクスを表現できます:
case class ListNode[+T](h: T, t: ListNode[T]) { ... }
残念ながら、共変の変位指定は型変数を共変の位置で使う場合のみ可能なので、このプログラムはコンパイルできません。型変数 T はメソッド prepend のパラメータ型として現われるので、この規則は破られています。しかし、下限型境界の助けを借りれば、T が共変の位置のみに現れる prepend メソッドを実装できます。
次は対応するコードです:
case class ListNode[+T](h: T, t: ListNode[T]) {
def head: T = h
def tail: ListNode[T] = t
def prepend[U >: T](elem: U): ListNode[U] =
ListNode(elem, this)
}
新しい prepend メソッドは、少しばかり制約の少ない型を持つことに注意してください。これにより、たとえば、既に存在するリストの先頭にスーパー型のオブジェクトを追加できます。得られるリストは、このスーパー型のリストです。
次は、そのことを示すコードです:
object LowerBoundTest extends Application {
val empty: ListNode[Null] = ListNode(null, null)
val strList: ListNode[String] = empty.prepend("hello")
.prepend("world")
val anyList: ListNode[Any] = strList.prepend(12345)
}
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)
}
Scala ひと巡り : サブクラス化 (Subclassing)
Scala のクラスは拡張可能です。サブクラスの機構により、与えられたスーパークラスの全メンバーの継承と、クラスメンバの追加定義によって、クラスを特化できます。
次は 1 つの例です:
class Point(xc: Int, yc: Int) {
val x: Int = xc
val y: Int = yc
def move(dx: Int, dy: Int): Point =
new Point(x + dx, y + dy)
}
class ColorPoint(u: Int, v: Int, c: String) extends Point(u, v) {
val color: String = c
def compareWith(pt: ColorPoint): Boolean =
(pt.x == x) && (pt.y == y) && (pt.color == color)
override def move(dx: Int, dy: Int): ColorPoint =
new ColorPoint(x + dy, y + dy, color)
}
この例では最初に、場所を表す新しいクラス Point を定義します。次に、クラス Point を拡張するクラス ColorPoint を定義します。
これは次のような結果になります:
- クラス ColorPoint は、そのスーパークラス Point からすべてのメンバーを継承します。;この場合、メソッド move と同様、値 x、y も継承します。
- サブクラス ColorPoint は、(継承した)メソッドの集合に新しいメソッド compareWith を加えます。
- Scala ではメンバー定義をオーバライドできます; この場合、クラス Point から継承した move メソッドをオーバライドします。これにより、ColorPoint オブジェクトのクライアントは、クラス Point の move メソッドにアクセスできなくなります。クラス ColorPoint 内では、継承されたメソッド move は スーパー呼び出し: super.move(...) を使ってアクセスできます。メソッドオーバーライドが非変の(つまり、オーバーライドするメソッドが同じシグニチャを持たなければならない) Java と異なり、Scala では反変/共変方式でメソッドをオーバライドできます。上の例では、このフィーチャーを利用してメソッド move に、スーパークラス Point で指定された Point オブジェクトを返す代わりに ColorPoint オブジェクトを返させています。
- サブクラスはサブ型を定義します; このことは今の場合、Point オブジェクトが必要とされるときはいつでも、ColorPoint オブジェクトを使えることを意味します。
複数の他クラスを継承したい場合、純粋なサブクラス化ではなく、ミックスインベースのクラス合成
[5] を利用する必要があります。
Scala ひと巡り : ローカルな型推論 (Local Type Inference)
Scala は、プログラマが明白なアノテーション型を書かなくても済む、組み込みの型推論機構を持っています。たとえば Scalaでは、変数の型を指定する必要がないことがよくあります。なぜなら、コンパイラが変数の初期化式から型を推論できるからです。同様にメソッドの戻り値型もしばしば省略できます。なぜなら、それらは本体の型に対応するので、コンパイラが推論できるからです。
次は 1 つの例です:
object InferenceTest1 extends Application {
val x = 1 + 2 * 3 // x の型は Int
val y = x.toString() // y の型は String
def succ(x: Int) = x + 1 // メソッド succ は Int 値を返す
}
再帰的なメソッドについては、コンパイラは結果型を推論できません。次のプログラムは、この理由でコンパイルできません。
object InferenceTest2 {
def fac(n: Int) = if (n == 0) 1 else n * fac(n - 1)
}
多相的メソッド
[25]を呼ぶあるいは、ジェネリッククラス
[16]をインスタンス化するとき、型パラメータの指定も必須ではありません。Scala コンパイラは、コンテキストや実際のメソッド/コンストラクタ・パラメータの型からそのような記述されていない型パラメータを推論します。
次はこのことを示す例です:
case class MyPair[A, B](x: A, y: B);
object InferenceTest3 extends Application {
def id[T](x: T) = x
val p = new MyPair(1, "scala") // 型: MyPair[Int, String]
val q = id(1) // 型: Int
}
上記プログラムの最後の 2 行は、推論された型をすべて明示した、次のコードに等価です:
val x: MyPair[Int, String] = new MyPair[Int, String](1, "scala")
val y: Int = id[Int](1)
ある状況では、次のプログラムが示すように、Scala の型推論機構に頼ることは非常に危険です。
object InferenceTest4 {
var obj = null
obj = new Object()
}
このプログラムはコンパイルできません。なぜなら、変数 obj の推論される型は Null だからです。この型の唯一の値は nullですから、この変数に他の値を参照させることはできません。
Scala ひと巡り : 統合された型 (Unified Types)
Java と異なり、Scala ではすべての値はオブジェクトです(数値や関数を含めて)。Scala はクラスを基盤にしているので、すべての値はクラスのインスタンスです。下図は Scala のクラス階層を示しています。
imageプラグインエラー : 画像を取得できませんでした。しばらく時間を置いてから再度お試しください。
全てのクラスのスーパークラスである scala.Any は直下にサブクラス scala.AnyValue と AnyRef を持っています。それらは 2 つの異なるクラスの系統 : 値クラスと参照クラスを代表しています。すべての値クラスは事前定義されています;それらは Java ライクな言語のプリミティブ型に対応します。他のすべてのクラスは参照型を定義します。ユーザー定義のクラスは、デフォルトでは参照型を定義します; つまり、それらはいつも(間接的に)scala.AnyRef のサブクラスとなります。Scala のユーザー定義クラスはすべて、暗黙のうちにトレイト scala.ScalaObject を拡張(継承)します。Scala 実行基盤からのクラス(つまり Java 実行時環境)は、scala.ScalaObject を拡張しません。もし Scala を Java 実行時環境のコンテキスト中で使うなら、scala.AnyRef は java.lang.Object に対応します。
上図は、値クラス間のビュー
[24]と呼ばれる暗黙の型変換も示していることに注意してください。
次の例は、数、文字、論理値等と関数の双方とも、他のオブジェクトとまったく同じく、オブジェクトであることを示しています。
object UnifiedTypes {
def main(args: Array[String]) {
val set = new scala.collection.mutable.HashSet[Any]
set += "This is a string" // 文字列の追加
set += 732 // 数の追加
set += 'c' // 文字の追加
set += true // 論理値の追加
set += main _ // main 関数の追加
val iter: Iterator[Any] = set.elements
while (iter.hasNext) {
println(iter.next.toString())
}
}
}
プログラムは、トップレベルのシングルトンオブジェクトの形で、main メソッドをもつアプリケーション UnifiedTypes を宣言します。main メソッドは、クラス HashSet[Any]のインスタンスを参照するローカル変数 set を定義します。プログラムはこの set に様々な要素を加えます。要素は、宣言されたセット要素型 Any に適合しなければなりません。最後に、すべての要素の文字列表現を印字します。
次はプログラムの出力です:
c
true
<function>
732
This is a string
Scala ひと巡り : 変位指定 (Variances)
Scala は、ジェネリッククラスの型パラメータ
[16]の変位指定アノテーションをサポートします。Java5 (aka.JDK 1.5
[58])と対照して、クラス抽象を定義するときに変位指定アノテーションを加えることができます。他方、Java5 では、変位指定アノテーションはクラス抽象を使うときにクライアントが与えます。(訳注:Scalaでは静的に表現できるということ)
ジェネリッククラスについてのページに、ミュータブル(更新可能)なスタックの例がありました。クラス Stack[T] によって定義された型は、型パラメータについて非変のサブ型付けになると説明しました。それによりクラス抽象の再利用を制限できます。今度はこの制限を持たないスタックの関数型(つまり、イミュータブル(更新不可)な)実装を導きます。これが、明白ではない方法で多相的メソッド
[25]、下限型境界
[19]、そして共変の型パラメータアノテーションの使用を結びつける、進んだ例であることに注意してください。さらにまた、スタック要素を明示的なリンクなしでチェインするために、内部クラス
[20]を利用します。
class Stack[+A] {
def push[B >: A](elem: B): Stack[B] = new Stack[B] {
override def top: B = elem
override def pop: Stack[B] = Stack.this
override def toString() = elem.toString() + " " +
Stack.this.toString()
}
def top: A = error("no element on stack")
def pop: Stack[A] = error("no element on stack")
override def toString() = ""
}
object VariancesTest extends Application {
var s: Stack[Any] = new Stack().push("hello");
s = s.push(new Object())
s = s.push(7)
Console.println(s)
}
アノテーション +T は、型 T が共変の位置でだけ使われると宣言します。同様に、-T は、T が反変の位置でだけ使われると宣言します。共変の型パラメータなので、この型パラメータについて共変のサブ型関係を得ます。この例では、もし T が S のサブ型なら、このことは、Stack[T] が Stack[S]のサブ型であることを意味します。 - とタグ付けられた型パラメータについては、反対のことが当てはまります。スタックの例では、メソッド push を定義できるようにするために、反変の位置で共変型パラメータ T を使う必要があります。スタックは共変のサブ型付けにしたいのですから、トリックを使います。メソッド push のパラメータ型上で抽象化します。push の型変数の下限境界として要素型 T を使う多相的メソッド
[25]を得ます。これには、その共変型パラメータとしての宣言と協調する T の変位指定を持ち込む効果があります。今、スタックは共変です。しかしこのソリューションによれば、たとえば、整数スタック上に文字列をプッシュすることが可能となります。戻り値は型 Stack[Any] のスタックです。;整数スタックを期待するコンテキスト中で戻り値を使う場合にだけ、実際にエラーを検出します。そうでない場合、より汎用的な要素型のスタックを得ます。
Scala ひと巡り : ビュー (Views)
暗黙のパラメータ
[63]とメソッドは、ビューと呼ばれる暗黙の型変換も定義できます。型 S から型 T へのビューは、関数型 S => T を持つ暗黙の値によって、あるいは、その型の値に変換可能なメソッドによって定義されます。
ビューは 2 つの状況で適用されます:
- 式 e が型 T で、T が式の要請型(期待される型) pt に適合しないとき
- 型 T の e の選択 e.m 中で、セレクタ m が T のメンバーを意味しないとき
最初の場合は、 e に適用可能でその結果型が pt に適合するビュー v が検索されます。2 番目の場合は、e に適用可能でその結果型が m という名前のメンバーを含むビュー v が検索されます。
型 List[Int]の 2 つのリスト xs と ys 上の、次の操作は正しいです。
xs <= ys
ただし、下記で定義された 暗黙のメソッド list2ordered と int2ordered がスコープ中にあると仮定して:
implicit def list2ordered[A](x: List[A])
(implicit elem2ordered: a => Ordered[A]): Ordered[List[A]] =
new Ordered[List[A]] { /* .. */ }
implicit def int2ordered(x: Int): Ordered[Int] =
new Ordered[Int] { /* .. */ }
list2ordered 関数は、型パラメータの可視境界(view bound:ビュー境界)を使っても表現できます:
implicit def list2ordered[A <% Ordered[A]](x: List[A]): Ordered[List[A]] = ...
Scala コンパイラはこのとき、上記で与えられた list2ordered の定義に等価なコードを生成します。
暗黙のうちにインポートされる
[64] オブジェクト scala.Predef は、いくつかの事前定義された型(たとえば Pair)とメソッド(たとえば error)ばかりでなく、いくつかのビューも宣言します。次の例は、事前定義されたビュー charWrapper のアイデアを示します:
final class RichChar(c: Char) {
def isDigit: Boolean = Character.isDigit(c)
// isLetter, isWhitespace, etc.
}
object RichCharTest {
implicit def charWrapper(c: char) = new RichChar(c)
def main(args: Array[String]) {
println('0'.isDigit)
}
}
Scala ひと巡り : XML 処理 (XML Processing)
Scala を使えば、XML ドキュメントの生成、解析、処理が簡単に行えます。Scala では、XML データをジェネリックなデータ表現を使うことであるいは、データに特化したデータ表現を用いて表現できます。後者の方法は、データ結合ツール schema2src によってサポートされます。
実行時表現
XML データはラベル付きのツリーとして表現されます。Scala 1.2から始まり(以前のバージョンでは -Xmarkup オプションを使う必要があります)、そのようなラベル付きノードを標準的な XML 構文を使って簡単に生成できます。
次の XML ドキュメントについて考えます。:
<html>
<head>
<title>Hello XHTML world</title>
</head>
<body>
<h1>Hello world</h1>
<p><a href="http://scala-lang.org/">Scala</a> talks XHTML</p>
</body>
</html>
このドキュメントは、次の Scala プログラムで生成できます。:
object XMLTest1 extends Application {
val page =
<html>
<head>
<title>Hello XHTML world</title>
</head>
<body>
<h1>Hello world</h1>
<p><a href="scala-lang.org">Scala</a> talks XHTML</p>
</body>
</html>;
println(page.toString())
}
Scala 式と XML をミックスできます。
object XMLTest2 extends Application {
import scala.xml._
val df = java.text.DateFormat.getDateInstance()
val dateString = df.format(new java.util.Date())
def theDate(name: String) =
<dateMsg addressedTo={ name }>
Hello, { name }! Today is { dateString }
</dateMsg>;
println(theDate("John Doe").toString())
}
データの結合
多くの場合、人は処理したい XML ドキュメントに関する DTD を持っています。そのための特別な Scala クラスを生成し、XML を解析し保存するためのコードがほしいことでしょう。Scala は、DTD を Scalaクラス定義 のコレクションに変えるといったことをあなたに代わって全部やってくれる、気のきいたツールになります。
schema2src ツールを使った文書化と例は、Burak のドラフト Scala xml book
[65] にあることを書き留めておきます。
最終更新:2011年03月20日 15:41