Scala ひと巡り : ジェネリッククラス (Generic Classes)
Java 5 (aka.JDK 1.5
[58])と同じように、Scala は、型がパラメータ化されたクラスを組み込でサポートしています。そのようなジェネリッククラスは、コレクションクラスの開発に特に役立ちます。
次はこのことを示す例です:
class Stack[T] {
var elems: List[T] = Nil
def push(x: T) { elems = x :: elems }
def top: T = elems.head
def pop() { elems = elems.tail }
}
クラス Stack は、任意の要素型 T の命令型(ミュータブル:更新可能な)スタックをモデル化します。型パラメータの使用により、正しい要素(つまり型が T のもの)だけがスタック上に push されるようにできます。同様に、型パラメータを用いてメソッド top が、与えられた型の要素だけを出力することを表現できます。
次は使用方法を示すいくつかの例です:
object GenericsTest extends Application {
val stack = new Stack[Int]
stack.push(1)
stack.push('a')
println(stack.top)
stack.pop()
println(stack.top)
}
次は、このプログラムの出力です。:
97
1
ジェネリック型のサブ型付けは、非変であることに注意してください。これは、もし型 Stack[Char] の文字スタックがあっても、それを型 Stack[Int] の整数スタックとしては使えないことを意味します。それでは、本当の整数を文字スタックに入れることがでてしまうので、不健全でしょう。結論を言えば、ただ S = T の場合に限り、Stack[T] は Stack[S] のサブ型になります。これは非常に制約が厳しいので、Scala は、ジェネリック型のサブ型付けの振る舞いをコントロールする、型パラメータのアノテーション機構
[17]を提供しています。
Scala ひと巡り : 暗黙のパラメータ (Implicit Parameters)
暗黙のパラメータをもつメソッドは、通常のメソッドとまったく同じように引数に適用されます。この場合、implicit ラベルは効果を持ちません。しかし、もしメソッドにその暗黙のパラメータに対する引数が書かれていないなら、そのような引数は自動的に供給されます。
暗黙のパラメータに渡すに適した実際の引数は、2 つのカテゴリに分けられます。
- 1 つめは、メソッド呼び出しの時点でアクセスできる、前置子のつかない、implicit 定義あるいは暗黙のパラメータを表す、すべての識別子 x が適しています。
- 2 つめは、implicit と印された暗黙のパラメータの型のコンパニオンモジュールのすべてのメンバーも適しています。
次の例は、monoid の add と unit 操作を使ってリストの要素の合計を計算する、メソッド sum を定義しています。暗黙の値がトップレベルではいけないことに注意してください。それらはテンプレートのメンバーでなければなりません。
abstract class SemiGroup[A] {
def add(x: A, y: A): A
}
abstract class Monoid[A] extends SemiGroup[A] {
def unit: A
}
object ImplicitTest extends Application {
implicit object StringMonoid extends Monoid[String] {
def add(x: String, y: String): String = x concat y
def unit: String = ""
}
implicit object IntMonoid extends Monoid[Int] {
def add(x: Int, y: Int): Int = x + y
def unit: Int = 0
}
def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
if (xs.isEmpty) m.unit
else m.add(xs.head, sum(xs.tail))
println(sum(List(1, 2, 3)))
println(sum(List("a", "b", "c")))
}
次は Scala プログラムの出力です:
6
abc
Scala ひと巡り : 内部クラス (Inner Classes)
Scala では、クラスは他のクラスをメンバーとして持てます。Java ライクな言語では、そのような内部クラスは取り囲むクラスのメンバーですが、Scala では反対に、そのような内部クラスは外側のオブジェクトへ束縛されます。違いを明らかにするために、グラフデータ型の実装概要を示します:
class Graph {
class Node {
var connectedNodes: List[Node] = Nil
def connectTo(node: Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}
このプログラムで、グラフはノードのリストによって表されます。ノードは内部クラス Node のオブジェクトです。各ノードは 隣のリストを持っており、それはリスト connectedNodes 中に記憶されます。これでいくつかのノードをもち、ノードを付加的に(incrementally)連結できるグラフをセットアップできます。
object GraphTest extends Application {
val g = new Graph
val n1 = g.newNode
val n2 = g.newNode
val n3 = g.newNode
n1.connectTo(n2)
n3.connectTo(n1)
}
次に、定義されるエンティティの型が何であるかを明示する型を用いて、例を補強します:
object GraphTest extends Application {
val g: Graph = new Graph
val n1: g.Node = g.newNode
val n2: g.Node = g.newNode
val n3: g.Node = g.newNode
n1.connectTo(n2)
n3.connectTo(n1)
}
このコードは、ノードの型が、その外側のインスタンス(この例ではオブジェクト g )で前置されることを明らかにしています。今 2 つのグラフがあるとして、Scala の型システムは、一方のグラフ内で定義されたノードと他方のグラフのノードを混ぜることを許しません。なぜなら、他のグラフのノードは異なる型をもつからです。
次は不正なプログラムです:
object IllegalGraphTest extends Application {
val g: Graph = new Graph
val n1: g.Node = g.newNode
val n2: g.Node = g.newNode
n1.connectTo(n2) // 正しい
val h: Graph = new Graph
val n3: h.Node = h.newNode
n1.connectTo(n3) // 不正!
}
Javaでは、上記サンプルプログラムの最後の行は正しいことに注意してください。Java は両方のグラフのノードに同じ型 Graph.Node を割り当てます。すなわち、Node はクラス Graph で前置されます(*1)。 Scalaでは、そのような型も表現でき、Graph#Node と書きます。もし異なるグラフのノードを連結できるようにしたければ、最初のグラフ実装の定義を次のように変える必要があります。
class Graph {
class Node {
var connectedNodes: List[Graph#Node] = Nil
def connectTo(node: Graph#Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}
このプログラムが、2 つの異なるグラフへのノードの張りつけを許さないことに注意してください。もしこの制限もなくしたければ、変数 nodes の型とメソッド newNode の戻り値型を Graph#Node に変える必要があります。
(*1)訳注: Scalaではクラス Graph ではなくオブジェクト g が前置されていて、 g.Node と h.Node はパスが異なるので異なる型を表すということ。
Scala ひと巡り : ミックスインクラス合成 (Mixin Class Composition)
単一継承のみをサポートする言語と対照して、Scala には、クラス再利用のためのより汎用的な概念があります。Scala では、クラスの新しいメンバー定義(すなわち、スーパークラス継承の差分)を再利用できます。これはミックスインクラス合成として表現されます。イテレータに関する次の抽象化を考えてみます。
abstract class AbsIterator {
type T
def hasNext: Boolean
def next: T
}
次に、メソッド foreach を用いて AbsIterator を展開するミックスインクラスについて考えます。ここで foreach は、イテレータが返す各要素に与えられた関数を適用します。ミックスインとして使用できるクラスを定義するのに、キーワード trait を使います。
trait RichIterator extends AbsIterator {
def foreach(f: T => Unit) { while (hasNext) f(next) }
}
次は、与えられた文字列の文字を次々に返す、具象イテレータクラスです:
class StringIterator(s: String) extends AbsIterator {
type T = Char
private var i = 0
def hasNext = i < s.length()
def next = { val ch = s charAt i; i += 1; ch }
}
StringIterator と RichIterator の機能を 1 つのクラスへ統合したいとします。これは単一継承とインタフェースだけではできません。両クラスともコードを伴うメンバー実装を含むからです。Scala では、ミックスインクラス合成を使ってできます。プログラマはクラス定義の差分 --- すなわち、継承さていれないすべての新しい定義 --- を再利用できます。この機構により、次のテストプログラムのようにして、RichIterator と StringIterator を統合できます。次は、与えられた文字列のすべての文字のカラムを印字します。
object StringIteratorTest {
def main(args: Array[String]) {
class Iter extends StringIterator(args(0)) with RichIterator
val iter = new Iter
iter foreach println
}
}
関数 main 中の Iter クラスは、親の StringIterator と RichIterator をキーワード with を使ってミックスイン合成し、構築されています。最初の親は Iter のスーパークラスと呼ばれるのに対し、2 つめ(と、もしあればその他すべて)の親は、ミックスインと呼ばれます。
Scala ひと巡り : 関数のネスト (Nested Functions)
Scala では、関数定義をネストできます。次のオブジェクトは、整数のリストから閾値未満の値を抽出する、filter 関数を提供します:
object FilterTest extends Application {
def filter(xs: List[Int], threshold: Int) = {
def process(ys: List[Int]): List[Int] =
if (ys.isEmpty) ys
else if (ys.head < threshold) ys.head :: process(ys.tail)
else process(ys.tail)
process(xs)
}
println(filter(List(1, 9, 2, 8, 3, 7, 4), 5))
}
ネストされた関数 process が、filter のパラメータ値である外側のスコープ中で定義された変数 threshold を参照することに注意してください。
次はこのプログラムの出力です:
List(1,2,3,4)
Scala ひと巡り : 無名関数の構文 (Anonymous Function Syntax)
Scala は無名関数の定義について、比較的簡単な構文を提供します。次の式は、整数の後続関数を生成します:
(x: Int) => x + 1
これは次の、無名クラス定義の略記表現です:
new Function1[Int, Int] {
def apply(x: Int): Int = x + 1
}
複数のパラメータをもつ関数も定義できます。
(x: Int, y: Int) => "(" + x + ", " + y + ")"
あるいは、パラメータなしの関数:
() => { System.getProperty("user.dir") }
関数の型を書くためのたいへん簡単な方法もあります。次は、上で定義した 3 つの関数の型です:
Int => Int
(Int, Int) => String
() => String
この構文は、次の型の略記表現です:
Function1[Int, Int]
Function2[Int, Int, String]
Function0[String]
Scala ひと巡り : カリー化 (Currying)
メソッドは複数のパラメータリストを定義できます。メソッドは、パラメータリストの数より少ない形で呼び出される時には、その引数としてパラメータリストを失った関数をもたらします。
次は 1 つの例です:
object CurryTest extends Application {
def filter(xs: List[Int], p: Int => Boolean): List[Int] =
if (xs.isEmpty) xs
else if (p(xs.head)) xs.head :: filter(xs.tail, p)
else filter(xs.tail, p)
def modN(n: Int)(x: Int) = ((x % n) == 0)
val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
println(filter(nums, modN(2)))
println(filter(nums, modN(3)))
}
2 つの filter 呼び出しにおいて、メソッド modN が部分適用されることに注意してください; すなわち、その最初の引数だけが実際に適用されます。項 modN(2) は、型 Int => Boolean の関数をもたらし、それはこのように、関数 filter の 2 番目の引数に対する可能な候補となります。
次は、上記プログラムの出力です:
List(2,4,6,8)
List(3,6)
Scala ひと巡り : 型依存クロージャの自動構築 (Automatic Type-Dependent Closure Construction)
uction)
Scala では、メソッドのパラメータにパラメータなしの関数名を与えることができます。そのようなメソッドが呼ばれる時、パラメータなしの関数名に対する実際のパラメータは評価されず、代わりにパラメータなしの関数が渡されます。これは対応するパラメータの計算をカプセル化します(いわゆる、名前呼出し評価)。
次のコードはこのメカニズムを示します:
object TargetTest1 extends Application {
def whileLoop(cond: => Boolean)(body: => Unit): Unit =
if (cond) {
body
whileLoop(cond)(body)
}
var i = 10
whileLoop (i > 0) {
println(i)
i -= 1
}
}
関数 whileLoop は、2 つのパラメータ cond と body をとります。関数の適用時、実際のパラメータは評価されません。しかしその代わりに、whileLoop の本体中で形式上のパラメータが使われる毎に、暗黙のうちに生成されるパラメータなしの関数が評価されます。このように、このメソッド whileLoop は、再帰的な実装方式の Java ライクな while-loop を実装します。
中置/後置演算子
[27] とこのメカニズムを結びつけて、(洗練された構文を用いた)より複雑な文を作れます。
次は loop-unless 文の実装です:
object TargetTest2 extends Application {
def loop(body: => Unit): LoopUnlessCond =
new LoopUnlessCond(body)
protected class LoopUnlessCond(body: => Unit) {
def unless(cond: => Boolean) {
body
if (!cond) unless(cond)
}
}
var i = 10
loop {
println("i = " + i)
i -= 1
} unless (i == 0)
}
loop 関数はただループの本体を受けつけるだけであり、そして(この本体オブジェクトをカプセル化する)クラス LoopUnlessCond のインスタンスを返します。本体がまだ評価されないことに注意してください。クラス LoopUnlessCond はメソッド unless を持っており、それを中置演算子として使用できます。このように、新たなループ : loop { < stats > } unless ( < cond > ) の、極めて自然な構文を作れます:
次は、TargetTest2 を実行したときの出力です:
i = 10
i = 9
i = 8
i = 7
i = 6
i = 5
i = 4
i = 3
i = 2
i = 1
Scala ひと巡り : オペレータ (Operators)
Scala では、ただ 1 つのパラメータをとるどのようなメソッドも中置演算子として使えます。
次は、3 つのメソッド and、or と negate を定義する classMyBool の定義です。
class MyBool(x: Boolean) {
def and(that: MyBool): MyBool = if (x) that else this
def or(that: MyBool): MyBool = if (x) this else that
def negate: MyBool = new MyBool(!x)
}
ここで、and と or を中置演算子として使えます。:
def not(x: MyBool) = x negate; // ここではセミコロンが必要
def xor(x: MyBool, y: MyBool) = (x or y) and not(x and y)
このコードの最初の行のように、パラメータなしのメソッドを後置演算子としても使用えます。2 番目の行は、新しい not 関数と同様に、and および or メソッドを使って xor 関数を定義します。この例で、中置演算子を使って xor 定義がいっそう理解しやすくなっています。
次は、より伝統的なオブジェクト指向プログラミング言語構文における、対応するコードです:
def not(x: MyBool) = x.negate; // semicolon required here
def xor(x: MyBool, y: MyBool) = x.or(y).and(x.and(y).negate)
Scala ひと巡り : 高階関数 (Higher-Order Functions)
Scala では、高階関数を定義できます。それらは、パラメータに他の関数をとるか、あるいは、その結果が関数であるような、関数です。
次は、他の関数 f と値 v をとり、関数 f を v に適用する、関数 apply です。
def apply(f: Int => String, v: Int) = f(v)
もしコンテキスト上で必要なら、メソッドが関数へ自動的に特化(coerced:強制)されることに注意してください。
次は 1 つの例です:
class Decorator(left: String, right: String) {
def layout[A](x: A) = left + x.toString() + right
}
object FunTest extends Application {
def apply(f: Int => String, v: Int) = f(v)
val decorator = new Decorator("[", "]")
println(apply(decorator.layout, 7))
}
実行すると次の出力となります。:
[7]
この例で、メソッド decorator.layout は、メソッド apply が必要とするときに、型 Int => String の値へ自動的に特化されます。メソッド decorator.layout が多相的メソッドであり(すなわち、いくつかのシグニチャ型にわたっての抽象化)、Scala コンパイラは最初にそのメソッド型を適切にインスタンス化しなければならないことに注意してください。
Scala ひと巡り : パッケージ (Packages)
パッケージはメンバークラス、オブジェクトとパッケージの集合を定義する特別なオブジェクトです。他のオブジェクトと異なり、パッケージは定義では導入できません。
パッケージング package p { ds } は、ds 中のすべての定義をその限定修飾名が p であるパッケージに、メンバーとして注入します。パッケージのメンバーはトップレベル定義と呼ばれます。もし ds 中の定義が private と印されていれば、そのパッケージ中の他のメンバーに対してのみ可視となります。
- protected 修飾子をパッケージ識別子 p で限定できます(たとえば protected[p])。そのような修飾子を印されたメンバーは、パッケージ p 内のすべてのコードから同様にアクセス可能です。
p からの選択 p.m は、p からのインポートと同様、オブジェクトに関して機能します。しかし、他のオブジェクトと異なり、パッケージは値としては使用できません。モジュールあるいはクラス名と、同じ完全修飾名のパッケージは不正です。
パッケージング外でのトップレベル定義は、特別な空パッケージに注入されるとみなされます。このパッケージは、名前を付けることはできず、したがってインポートできません。しかし、空パッケージのメンバーは互いに限定修飾なしで可視です。
パッケージ節ではじまるコンパイル単位 package p ; stats は、ただ 1 つのパッケージング package p { stats } からなるコンパイル単位と同じです。
同じ Scala ソースファイル中で、複数のパッケージを宣言できます。
package p1 {
object test extends Application {
println("p1.test")
}
}
package p2 {
object test extends Application {
println("p2.test")
}
}
インポート節
インポート節は、import p.I の形をしています。ここで、インポート式 I は、限定修飾なしでアクセスできる、p のインポート可能なメンバー名の集合を決定します。たとえば:
節 限定修飾なしで利用可能
import p._ p の全てのメンバー (Javaにおける import p.* と類似)
import p.x p のメンバー x
import p.{x => a} p のメンバー x を a にリネーム
import p.{x, y} p のメンバー x と y
import p1.p2.z p2 のメンバー z 。p2 自身は p1 のメンバー
また、節 import p1._ , p2._ は、import p1._; import p2._ の略記表現です。
次は、すべてのコンパイル単位中に暗黙のうちに、この順番でインポートされます。
- パッケージ java.lang、
- パッケージ Scala と
- オブジェクト scala.Predef
この順番において、後でインポートしたメンバーは、前にインポートしたメンバーを隠します。
Scala ひと巡り : パターンマッチング (Pattern Matching)
Scala には、汎用的なパターンマッチング機構が組み込まれています。ファーストマッチ方式で、あらゆる種類のデータ上でマッチングさせることができます。
次は、整数値に対するマッチ方法を示す、小さな例です:
object MatchTest1 extends Application {
def matchTest(x: Int): String = x match {
case 1 => "one"
case 2 => "two"
case _ => "many"
}
println(matchTest(3))
}
ケース文のブロックは、整数を文字列にマップする関数を定義します。match キーワードは、オブジェクトに関数を適用する(上記のパターンマッチング関数のような)便利な方法を提供します。
次は、異なる型のパターンの値とマッチする、2 つめの例です:
object MatchTest2 extends Application {
def matchTest(x: Any): Any = x match {
case 1 => "one"
case "two" => 2
case y: Int => "scala.Int"
}
println(matchTest("two"))
}
もし x が整数値 1 を参照するなら、最初のケースがマッチします。もし x が文字列 "two" と等しければ、2 つめのケースがマッチします。3 つめのケースは型付きパターンから成ります; それは任意の整数にマッチし、セレクター値 x を整数型の変数 y に束縛します。
Scala のパターンマッチング文は、ケースクラス
[10]を介して表現される代数型上のマッチングにおいて、最も役に立ちます。
Scala ではまた、抽出子オブジェクト
[59]中の unapply メソッドを使って、ケースクラスとは独立にパターンを定義できます。
Scala ひと巡り : 多相的メソッド (Polymorphic Methods)
Scala 中のメソッドは、値と型の両方でパラメータ化できます。クラスレベルのように、値パラメータは丸括弧の対で囲み、他方、型パラメータは対の角括弧内で宣言します。
次は 1 つの例です:
object PolyTest extends Application {
def dup[T](x: T, n: Int): List[T] =
if (n == 0) Nil
else x :: dup(x, n - 1)
println(dup[Int](3, 4))
println(dup("three", 3))
}
オブジェクト PolyTest 中のメソッド dup は、型 T と値パラメータ x: T、n: Int でパラメータ化されています。メソッド dup が呼び出されると、プログラマは必要とされるパラメータを提供しますが(上記プログラム中の 5 行目参照)、上記プログラムの 6 行目のように、プログラマは実際の型パラメータを明示的に与える必要はありません。Scala の型システムは、そのような型を推論できます。それは、与えられた値パラメータの型と、メソッドが呼ばれるコンテキストを調べることで、なされます。
トレイト Application は短いテストプログラムを書くために設計したのですが、JVM の出力コード最適化能力を動揺させるようなプログラムを書くことは避けるべきでしょう。この代わりに def main() を使ってください。
Scala ひと巡り : 正規表現パターン (Regular Expression Patterns)
右無視シーケンスパターン
右無視パターンは、Seq[A]のサブ型あるいは、(たとえば下記のような)反復する形式上のパラメータをもつケースクラスなどのデータ分解に役立つフィーチャーです。
Elem(prefix:String, label:String, attrs:MetaData,
scp:NamespaceBinding, children:Node*)
これらの場合、Scala は、任意長のシーケンスを表すワイルドカード-星印 _ * を最右端にもつパターンを許しています。
次は、シーケンスの前部とマッチし、残りを変数 restへ束縛する、パターンマッチの例です。
object RegExpTest1 extends Application {
def containsScala(x: String): Boolean = {
val z: Seq[Char] = x
z match {
case Seq('s','c','a','l','a', rest @ _*) =>
println("rest is "+rest)
true
case Seq(_*) =>
false
}
}
}
次に述べる理由により、前 Scala バージョンと違い、もう任意の正規表現は使用できません。
Scala から当面の間除かれた一般的な Regexp パターン
正当性の問題が見つかり、このフィーチャーは当面の間 Scala 言語から除かれています。もしユーザーコミュニティーからのリクエストがあれば、私たちはこれを改善した形で復活させるかもしれません。
私達の考えでは、正規表現パターンは私たちが見積もったほどには XML 処理に役立ちませんでした。現実の XML 処理アプリケーションでは、XPath ははるかに良い選択肢に思われます。変換処理あるいは正規表現パターンが、あまり使われずしかも取り除くことが難しい難解なパターンに対していくつかのバグをもつということを見つけたとき、言語を単純化する時が来たと私達は判断しました。
最終更新:2011年03月11日 10:07