Scala ひと巡り : 抽象型 (Abstract Types)
Scala では、クラスは値(コンストラクタ・パラメータ)と(もしクラスがジェネリック
[16]なら)型でパラメータ化されます。単に規則に従って、オブジェクトメンバーとして値を持てるというばかりではありません; 値と同様に、型はオブジェクトのメンバーです。さらに、メンバーの両形式とも、具象あるいは抽象で構いません。
次の例は、クラス Buffer のメンバーとして、延期された値定義と抽象型定義の両方を定義しています。
abstract class Buffer {
type T
val element: T
}
抽象型はその正体が正確には知られていない型です。上記の例で、我々は、クラス Buffer の各オブジェクトが 型メンバー T を持つことだけを知っています。しかしクラス Buffer の定義は、メンバー型 T がどのような具象(具体的な)型に対応するのかを明らかにしません。値定義と同じように、サブクラス中で型定義をオーバライドできます。これにより、(可能な、抽象型の具象インスタンス化を記述する)型境界を厳しくすることで、抽象型についてより多くの情報を明らかにできます。
次のプログラムで、型 T が新しい抽象型 U のサブ型でなければならないと述べることで、バッファ中にシーケンスのみを記憶できるクラス SeqBuffer を得ます:
abstract class SeqBuffer extends Buffer {
type U
type T <: Seq[U]
def length = element.length
}
抽象型メンバーをもつトレイトあるいはクラス
[2]は、無名クラスのインスタンス化との組合せでしばしば使われます。この例として、整数リストを参照するシーケンスバッファを扱う、次のプログラムを見てみます。:
abstract class IntSeqBuffer extends SeqBuffer {
type U = Int
}
object AbstractTypeTest1 extends Application {
def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
new IntSeqBuffer {
type T = List[U]
val element = List(elem1, elem2)
}
val buf = newIntSeqBuf(7, 8)
println("length = " + buf.length)
println("content = " + buf.element)
}
メソッド newIntSeqBuf の戻り値型は、型 U が toInt に等しい、トレイト Buffer の特化を参照します。メソッド newIntSeqBuf 本体内における無名クラスのインスタンス化で、似たような型エイリアスを使っています。そこでは、型 T が List[Int]を参照する、IntSeqBuffer の新しいインスタンスを生成します。
抽象型メンバーをクラスの型パラメータに変えることや、その逆も可能であることに注意してください。次は、上記コードの型パラメータだけを使うバージョンです:
abstract class Buffer[+T] {
val element: T
}
abstract class SeqBuffer[U, +T <: Seq[U]] extends Buffer[T] {
def length = element.length
}
object AbstractTypeTest2 extends Application {
def newIntSeqBuf(e1: Int, e2: Int): SeqBuffer[Int, Seq[Int]] =
new SeqBuffer[Int, List[Int]] {
val element = List(e1, e2)
}
val buf = newIntSeqBuf(7, 8)
println("length = " + buf.length)
println("content = " + buf.element)
}
ここでは変位指定アノテーション
[17]を使う必要があることに注意してください; そうでなければ、メソッド newIntSeqBuf が返すオブジェクトの具象シーケンス実装型を隠せなくなります。さらにまた、型パラメータを抽象型で置き換えできない場合があります。
Scala ひと巡り : アノテーション (Annotations)
アノテーションは、定義にメタ情報を関連づけます。 単純なアノテーション節は、@C あるいは @C(a1,...,an) の形です。ここで、C はクラス C のコンストラクタで、scala.Annotation
[31]に適合しなくてはなりません。
All given constructor_arguments a1,...,an must be constant_expressions (i.e., expressions on numeral literals, strings, class_literals, Java enumerations and one-dimensional arrays of them) .
与えられたコンストラクタ引数 a1,...,an はすべて、定数式(すなわち、数値リテラル、文字列、クラスリテラル、Java enumとそれらの 1 次元配列上の式)でなければなりません。
アノテーション節は、その後に続く、最初の定義または宣言に適用されます。1 つ以上のアノテーション節が定義や宣言に先行するかもしれません。それら節の与えられた順番は重要ではありません。
アノテーション節の意味は処理系依存です。Java プラットフォームでは、次の Scala アノテーションは標準的な意味を持っています。
Scala Java
scala.SerialVersionUID
[32] serialVersionUID
[33] (フィールド)
scala.cloneable
[34] java.lang.Cloneable
[35]
scala.deprecated
[36] java.lang.Deprecated
[37]
scala.inline
[38] (2.6.0から) 等価なもの無し
scala.native
[39] (2.6.0から) native
[40] (キーワード)
scala.remote
[41] java.rmi.Remote
[42]
scala.serializable
[43] java.io.Serializable
[44]
scala.throws
[45] throws
[40] (キーワード)
scala.transient
[46] transient
[40] (キーワード)
scala.unchecked
[47] (2.4.0から) 等価なもの無し
scala.volatile
[48] volatile
[40] (キーワード)
scala.reflect.BeanProperty
[49] Design pattern
[50]
次の例では、Java main プログラム中で例外送出をキャッチするために、メソッド read の定義に throws アノテーションを加えています。
Java コンパイラは、どのチェック例外がメソッドあるいはコンストラクタの実行によって引き起こされるか分析し、プログラムがチェック例外
[51]用のハンドラを含むことを確認します。起きる可能性のある各チェック例外に対して、メソッドあるいはコンストラクタの throw 節では、その例外クラスあるいはその例外クラスのスーパークラスの 1 つに言及しなくてはなりません。
Scala にはチェック例外がないので、Java コードが Scala メソッドの送出する例外を捕えることができるように、Scala メソッドに 1 つ以上の throws アノテーションをつける必要があります。
package examples
import java.io._
class Reader(fname: String) {
private val in = new BufferedReader(new FileReader(fname))
@throws (classOf [IOException])
def read() = in.read()
}
次の Java プログラムはファイルの内容を印字します。ファイル名は main メソッドに最初の引数として渡されます。
package test;
import examples.Reader; // Scala クラス !!
public class AnnotaTest {
public static void main(String[] args) {
try {
Reader in = new Reader(args[0]);
int c;
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
} catch (java.io.Exception e) {
System.out.println(e.getMessage());
}
}
}
クラス Reader 中の throws アノテーションをコメントアウトすると、Java main プログラムのコンパイル時に、次のエラーメッセージが出力されます:
Main.java:11: Exception java.io.IOException is never thrown in body of corresponding try statement
} catch (java.io.IOException e) {
^
1 error
Java アノテーション
注意 : Java アノテーションの -target:jvm-1.5 オプションの使用を確認してください。
Java 1.5 は、アノテーション
[53]の形でユーザー定義メタデータを導入しました。アノテーションの重要な特徴は、指定された名前と値の対を信頼して、それら要素を初期化することです。 例えば、もしあるクラスのソースを追跡するアノテーションが必要なら、次のように定義するかもしれません。
Java annotations @interface Source {
public String URL();
public String mail();
}
それを次のように適用してください
@Source(URL = "http://coders.com/",
mail = "support@coders.com")
public class MyClass extends HisClass ...
Scala のアノテーション適用は、Java アノテーションのインスタンス化のために名前付き引数を使う必要があるので、コンストラクタ呼び出しのように見えます。
@Source(URL = "http://coders.com/",
mail = "support@coders.com")
class MyScalaClass ...
もしアノテーションが(デフォルト値をもたない) ただ 1 つの要素を含むだけなら、この構文はたいへんうんざりします。そこで、規約により(by convention)、もし名前を値として指定するなら、それをコンストラクタに似た構文を使って Java 中で適用できます。:
@interface SourceURL {
public String value();
public String mail() default "";
}
それを次のように適用してください
@SourceURL("http://coders.com/")
public class MyClass extends HisClass ...
In this case, Scala provides the same possiblity.
この場合、Scala は同じことを提供します。
@SourceURL("http://coders.com/")
class MyScalaClass ...
mail 要素はデフォルト値を指定されているので、それに明示的に値を与える必要はありません。しかし、もしそうする必要があっても、Java 中で 2 つのスタイルを混ぜて適応させることはできません。:
@SourceURL(value = "http://coders.com/",
mail = "support@coders.com")
public class MyClass extends HisClass ...
Scala はこの点に関してより柔軟です。
@SourceURL("http://coders.com/",
mail = "support@coders.com")
class MyScalaClass ...
この拡張された構文は、.NET のアノテーションでも同じあり、それらアノテーションのフルの能力を引き出します。
Scala ひと巡り : クラス (Classes)
Scala のクラスは静的なテンプレートであり、実行時にたくさんのオブジェクトへインスタンス化されます。
次は、クラス Point を定義するクラス定義です:
class Point(xc: Int, yc: Int) {
var x: Int = xc
var y: Int = yc
def move(dx: Int, dy: Int) {
x = x + dx
y = y + dy
}
override def toString(): String = "(" + x + ", " + y + ")";
}
クラスは 2 つの変数 x と y、2 つのメソッド move と toString を定義します。move は 2 つの整数を引数にとりますが、値を返しません (暗黙の戻り値型 Unit は、Java ライクな言語の void に相当します)。他方、toString は引数をとらず、String 値を返します。toString は事前定義された toString メソッドをオーバライドするので、override フラグでタグ付けしなければなりません。
Scala のクラスは、コンストラクタ引数でパラメータ化されます。上記のコードは 2 つのコンストラクタ引数 xc と yc を定義します;それらは共にクラス本体全体で可視です。この例では、それらは変数 x と y の初期化に使われています。
クラスは、次の例が示すように、new プリミティブでインスタンス化できます:
object Classes {
def main(args: Array[String]) {
val pt = new Point(1, 2)
println(pt)
pt.move(10, 10)
println(pt)
}
}
このプログラムは実行可能なアプリケーション Classes を、main メソッドをもつトップレベルのシングルトンオブジェクトの形で定義します。main メソッドは新しい Point を生成し、それを値 pt に記憶します。val 構文で定義された値は更新が許されないという点が、var 構文(上記 クラス Point 参照)で定義された変数とは異なることに注意してください; すなわち、値(value)は不変です。
次はプログラムの出力です:
(1, 2)
(11, 12)
Scala ひと巡り : ケースクラス (Case Classes)
Scala はケースクラスの概念をサポートします。ケースクラスは通常のクラスであり、そのコンストラクタ・パラメータをエクスポートし、パターンマッチング
[11]を介して再帰的な分解メカニズムを提供します。
次は、1 つの抽象スーパークラス Term と 3 つの具象ケースクラス Var、Fun と App からなるクラス階層の例です。
abstract class Term
case class Var(name: String) extends Term
case class Fun(arg: String, body: Term) extends Term
case class App(f: Term, v: Term) extends Term
このクラス階層は、型付けされていない(untyped) λ計算
[54]の項を表現するのに使えます。ケースクラスのインスタンス構築にあたり、Scala では new プリミティブを使う必要がありません。単純に、クラス名を関数として使用できます。
次は 1 つの例です:
Fun("x", Fun("y", App(Var("x"), Var("y"))))
ケースクラスのコンストラクタ・パラメータは公開の値として扱われ、直接アクセスできます。
val x = Var("x")
Console.println(x.name)
すべてのケースクラスに対して、Scala コンパイラは、構造的等価性を実装する equals メソッドと toString メソッドを生成します。たとえば:
val x1 = Var("x")
val x2 = Var("x")
val y1 = Var("y")
println("" + x1 + " == " + x2 + " => " + (x1 == x2))
println("" + x1 + " == " + y1 + " => " + (x1 == y1))
は、次のように印字するでしょう。
Var(x) == Var(x) => true
Var(x) == Var(y) => false
もしパターンマッチングをデータ構造の分解に使うなら、ケースクラスを定義するのが妥当です。次のオブジェクトは、λ計算を表現するプリティプリンタ関数を定義します:
object TermTest extends Application {
def printTerm(term: Term) {
term match {
case Var(n) =>
print(n)
case Fun(x, b) =>
print("^" + x + ".")
printTerm(b)
case App(f, v) =>
Console.print("(")
printTerm(f)
print(" ")
printTerm(v)
print(")")
}
}
def isIdentityFun(term: Term): Boolean = term match {
case Fun(x, Var(y)) if x == y => true
case _ => false
}
val id = Fun("x", Var("x"))
val t = Fun("x", Fun("y", App(Var("x"), Var("y"))))
printTerm(t)
println
println(isIdentityFun(id))
println(isIdentityFun(t))
}
この例で関数 print は、パターンマッチング文として表現されており、それは match キーワードで始まる case Pattern => Body 節の並びから成っています。
上記のプログラムは、与えられた項が単純な識別関数に対応するかどうかチェックする関数 isIdentityFun も定義します。この例は、深いパターンとガードを使います。与えられた値をもつパターンとマッチした後、その(キーワード if の後に定義された)ガードが評価されます。もしそれが true を返すなら、マッチは成功です。;そうでなければ失敗であり、次のパターンが試みられます。
Scala ひと巡り : 事前定義された classOf 関数 (Predefined function classOf)
事前定義された関数 classOf[T]は、 Scala のクラス型 T の実行時表現を返します。次の Scala コード例は、 args パラメータの実行時表現を印字します:
object ClassReprTest {
abstract class Bar {
type T <: AnyRef
def bar(x: T) {
println("5: " + x.getClass())
}
}
def main(args: Array[String]) {
println("1: " + args.getClass())
println("2: " + classOf[Array[String]])
new Bar {
type T = Array[String]
val x: T = args
println("3: " + x.getClass())
println("4: " + classOf[T])
}.bar(args)
}
}
次は Scala プログラムの出力です:
1: class [Ljava.lang.String;
2: class [Ljava.lang.String;
3: class [Ljava.lang.String;
4: class [Ljava.lang.String;
5: class [Ljava.lang.String;
Scala ひと巡り : 複合型 (Compound Types)
ときには、オブジェクトの型が、他の複数の型のサブ型であると表現することが必要になります。Scala では、それをオブジェクト型の論理積である複合型(compound types)の助けを借りて表現できます。
2 つのトレイト Cloneable と Resetable があるとします。
trait Cloneable extends java.lang.Cloneable {
override def clone(): Cloneable = { super.clone(); this }
}
trait Resetable {
def reset: Unit
}
いま、オブジェクトを引数にとってクローンし、オリジナルのオブジェクトをリセットする関数 cloneAndReset を書きたいとします。
def cloneAndReset(obj: ?): Cloneable = {
val cloned = obj.clone()
obj.reset
cloned
}
パラメータ obj の型は何か、という問題が生じます。もしそれが Cloneable なら、オブジェクトはクローンできますがリセットできません。; しかしもし Resetable なら、リセットできますがクローン操作がありません。そのような状況で型キャストを避けるために、obj の型が Cloneable と Resetable の両方であると指定できます。Scala では、複合型を使って Cloneable with Resetable のように書きます。
次は、アップデートした関数です:
def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {
//...
}
複合型は複数のオブジェクト型からなり、ただ一つの細別(refinement)を持てます。細別は既存のオブジェクトメンバーのシグニチャを狭めるのに使えます。
一般的な書き方は A with B with C ... { refinement } です。
細別の使用例は、抽象型
[21]に関するページにあります。
Scala ひと巡り : シーケンス内包表記 (Sequence Comprehensions)
Scala は、シーケンス内包表記式に対して簡単な表記法を提供します。内包表記は for enums yield e の形をしています。ここで enums は、セミコロンで分離された列挙子のリストを参照します。列挙子は、新しい変数を導入する生成子、あるいは、フィルタです。内包表記は、列挙子 enum によって生成された各束縛にごとに、本体 e を評価し、それら値のシーケンスを返します。
次は 1 つの例です:
object ComprehensionTest1 extends Application {
def even(from: Int, to: Int): List[Int] =
for (i <- List.range(from, to) if i % 2 == 0) yield i
Console.println(even(0, 20))
}
関数 even 中の for 式は Int 型の新しい変数 i を導入し、それをリスト List(from,from + 1,...,to - 1) の全ての値へ次々に束縛します。ガード if i % 2 == 0 は、(式 i だけからなる)本体が偶数の場合のみ評価されるよう、全ての奇数をフィルターします。最終的に、for 式全体は偶数のリストを返します。
プログラムは次を出力します:
List(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
次はより複雑な例で、その合計が与えられた値 v と等しい、0 から n - 1 までの数の対をすべて計算します。:
object ComprehensionTest2 extends Application {
def foo(n: Int, v: Int) =
for (i <- 0 until n;
j <- i + 1 until n if i + j == v) yield
Pair(i, j);
foo(20, 32) foreach {
case (i, j) =>
println("(" + i + ", " + j + ")")
}
}
この例は、内包表記がリストに制限されないことを示しています。前のプログラムは、代わりにイテレータを使っています。(適切な型をもつ) 操作 filter、map そして flagMapをサポートするすべてのデータ型は、シーケンス内包表記中で使えます。
次はプログラムの出力です:
(13, 19)
(14, 18)
(15, 17)
シーケンス内包表記で Unitを返す、特別の形式もあります。そこでは、生成子のリストとフィルターから生成される束縛は、副作用を起こさせるために使われます。そのようなシーケンス内包表記を利用するには、プログラマはキーワード yield を取り除かなければなりません。
次は、前のものと等価ではあるが Unit を返す、特別な for 内包表記を使うプログラムです。:
object ComprehensionTest3 extends Application {
for (i <- Iterator.range(0, 20);
j <- Iterator.range(i + 1, 20) if i + j == 32)
println("(" + i + ", " + j + ")")
}
Scala ひと巡り : 抽出子オブジェクト (Extractor Objects)
Scala では、パターンをケースクラスとは独立に定義できます。この目的のために、unapply という名前のメソッドを定義することで、いわゆる抽出子がもたらされます。
例えば、次のコードは抽出子オブジェクト Twice を定義します。
object Twice {
def apply(x: Int): Int = x * 2
def unapply(z: Int): Option[Int] = if (z%2 == 0) Some(z/2) else None
}
object TwiceTest extends Application {
val x = Twice(21)
x match { case Twice(n) => Console.println(n) } // prints 21
}
次は、ここで関係する 2 つの文法上の規約(convention)です:
- パターン case Twice(n)は、Twice.unapply の呼び出しを引き起こし、偶数のマッチに使われます。; unapply の戻り値は、引数がマッチしたかどうか、そしてさらなるマッチングのために使えるサブ値を伝えます。ここで、サブ値は z/2 です。
- The apply method is not necessary for pattern matching. It is only used to mimic a constructor val x = Twice(21) expands to val x = Twice.apply(21) .
- apply メソッドは、パターンマッチングについては必須ではありません。これはただ、コンストラクタをまねて val x = Twice(21) を val x = Twice.apply(21) へ展開する時に使われるだけです。
unapply の戻り値型は、次のように選ぶべきです:
- もしそれが単なるテストなら、Boolean を返す。たとえば case even() 。
- もしそれがただ一つの、型 T のサブ値を返すなら、Option[T]を返す。
- もし複数のサブ値 T1,...,Tn を返したいなら、それらをまとめてタプルのオプション Option[(T1,...,Tn)]として返す。
しばしば、サブ値の数が固定で、シーケンスを返したいことがあります。その場合は、unapplySeq を介して同様にパターンを定義できます。最後のサブ値の型 Tn は、Seq[S]でなければなりません。このメカニズムは、たとえば、パターン case List(x1,...,xn) 中で使われます。
抽出子を使えば、コードはさらに保守しやすくなります。詳細は、Emir
[56]、Odersky
[57] と Williams (2007 年 1 月) らによる論文「パターンを用いたオブジェクトマッチング」
[55](4 章参照) を読んでください。
最終更新:2011年03月08日 21:09