構造的部分型(Structural Subtyping)
型の階層構造とは関係なく特定のメソッドを定義して、このメソッドを持つ型として定義できる。
Ruby や python のダックタイピングと同じようなもの
戻り値の型が Unit で引数無しの close メソッドを持つ型を構造的部分型として指定
def using[S <: { def close(): Unit }, U](s: S)(f: S => U): U = { try f(s) finally s.close }
scala では type の抽象型定義 で C の typedef と似たような抽象型を定義することができる
type Hoge = Foobar
抽象型に構造的部分型を定義することも可能
type Closeable2 = { def close(): Unit } def using[S <: Closeable2, U](s: S)(f: S => U): U = { try f(s) finally s.close }
長さによって、短い場合は直接指定して長くなる場合は抽象型に定義するとよさそう。
簡単なインターフェースは構造的部分型を使う。
上限境界、下限境界
上限境界、下限境界を指定して型パラメータまたは抽象型の型に制限をつけることができる。
上限境界(upper bounds)
[T :< Parent]
- T は、型 Parent のサブ型(子の型)を参照
- 型パラメータまたは抽象型の型のスーパー型(親の型: Parent)から許可
- サブ型への境界は Nothing まで。つまり型階層の一番下
trait Hoge { val item: String val t: String def getId: String = item + t } class Foo(val item: String) extends Hoge { val t = "foo" } class Bar(val item: String) extends Hoge { val t = "bar" } object Foo { def checkType[T <: Hoge](a: T, b: T) = a.t == b.t def checkItem[T <: Hoge](a: T, b: T) = a.item == b.item def check[T <: Hoge](a: T, b: T) = a.getId == b.getId def main(argv: Array[String]) { val foo1 = new Foo("2012") val foo2 = new Foo("2011") val bar = new Bar("2012") println(checkType(foo1, foo2)) println(checkType(foo1, bar)) println(checkItem(foo1, foo2)) println(checkItem(foo1, bar)) println(check(foo1, foo2)) println(check(foo1, bar)) } }
結果
$ scala Foo
true
false
false
true
false
false
下限境界(lower bounds)
[T >: Sub]
- T は、型 Sub のスーパー型(親の型)を参照
- 型パラメータまたは抽象型の型はサブ型(子の型: Sub)までを許可
- スーパー型への境界は Any まで。つまり型階層の一番上
class Foobar[+T](elem: T, elems: Foobar[T]) { def add[P >: T](item: P): Foobar[P] = new Foobar(item, this); def getList[P >: T](tail: List[P]): List[P] = { elems match { case null => elem :: tail case _ => elems.getList(elem :: tail) } } } object Foobar { def main(argv: Array[String]) { val foobar = new Foobar[AnyVal](0, null) val foobar2 = foobar.add(1).add(2).add(3).add(4); foobar2.getList(Nil).foreach(println) } }
結果
$ scala Foobar
0
1
2
3
4
型パラメータの変位指定(variance)
型パラメータで型の横に共変(+), 反変(-)の変位アノテーションを付けて変位指定ができる。
変位指定アノテーションを付けない場合は非変(nonvariant)になる。
どのパラメータ型を渡せるかの3種類の規則を指定。
共変(covariant)
scala> class Foo[+T](val a: T) defined class Foo scala> def bar(b: Foo[AnyVal]) = println(b.a) bar: (b: Foo[AnyVal])Unit scala> bar(new Foo[AnyVal](1)) 1 scala> bar(new Foo[Int](1)) 1 scala> bar(new Foo[Any](1)) <console>:10: error: type mismatch; found : Foo[Any] required: Foo[AnyVal] bar(new Foo[Any](1)) ^
- T型とTのサブ型は指定できるが、T のスーパー型は指定できない。
- ScalaのListの定義は共変。List[Int] は Lint[Any] の引数を受け取るメソッドに指定できる。
- イミュータブルなコレクションの型パラメータは共変が便利。
sealed abstract class List[+A] extends LinearSeq[A] with Product with GenericTraversableTemplate[A, List] with LinearSeqOptimized[A, List[A]] { ..snip..
非変(nonvariant)
scala> class Foo2[T](val a: T) defined class Foo2 scala> def bar2(b: Foo2[AnyVal]) = println(b.a) bar: (b: Foo2[AnyVal])Unit scala> bar2(new Foo2[AnyVal](1)) 1 scala> bar2(new Foo2[Int](1)) <console>:10: error: type mismatch; found : Foo2[Int] required: Foo2[AnyVal] Note: Int <: AnyVal, but class Foo2 is invariant in type T. You may wish to define T as +T instead. (SLS 4.5) bar(new Foo2[Int](1)) ^ scala> bar2(new Foo2[Any](1)) <console>:10: error: type mismatch; found : Foo2[Any] required: Foo2[AnyVal] Note: Any >: AnyVal, but class Foo2 is invariant in type T. You may wish to define T as -T instead. (SLS 4.5) bar2(new Foo2[Any](1)) ^
Scala の ArrayBuffer の定義は非変。
ミュータブルコレクションの型パラメータを非変にしておくと同じ型しかパラメータ型として指定できない制約をかけることができる。
class ArrayBuffer[A](override protected val initialSize: Int) extends Buffer[A] ..snip..
javaでは配列が共変でコレクションは非変。
反変(contravariant)
scala> class Foo3[-T] defined class Foo3 scala> class T1 defined class T1 scala> class T2 extends T1 defined class T2 scala> class T3 extends T2 defined class T3 scala> def bar3(a: Foo3[T2]) = println(a) bar3: (a: Foo3[T2])Unit scala> bar3(new Foo3[T1]) $line1.$read$$iw$$iw$Foo3@1ad9b0f scala> bar3(new Foo3[T2]) $line1.$read$$iw$$iw$Foo3@2445d7 scala> bar3(new Foo3[T3]) <console>:13: error: type mismatch; found : Foo3[T3] required: Foo3[T2] bar3(new Foo3[T3]) ^
関数(変換処理)の型の入力は反変、出力は共変 trait Funtcion1[-T1, +R] extends AnyRef
暗黙の型変換 (implicit conversion)
型変換用の関数の定義に implicit つけると型変換が必要な場面で自動的に型変換関数が実行される。
例えば forへ指定する Int to Int で暗黙の型変換が使用されている。
1 to 3 は RichInt の to メソッドから Range(1,2,3) が返される。
1 の Int は、暗黙の型変換で Int から RichInt に変換されて to メソッドが呼び出される。
scala> val foo = 1 to 3 foo: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)
to メソッド はscala.runtime.RichInt で定義されている。
final class RichInt(val self: Int) extends ScalaNumberProxy[Int] with RangedProxy[Int] { ..snip.. def to(end: Int): Range.Inclusive = Range.inclusive(self, end) ..snip.. }
Int から RichInt への暗黙の型変換関数 intWrapper は、scala.Predef へ extends している scala.LowPriorityImplicits で定義されている。
Int に定義がないメソッドが呼ばれた場合は RichInt で定義しているメソッドを探して見つかった時に RichInt へ変換して該当するメソッドが呼ばれる。
object Predef extends LowPriorityImplicits { ..snip.. }
LowPriorityImplicits には Int 以外にも Byte や Short 等から Rich.. 型への暗黙の型変換関数が用意されている。
class LowPriorityImplicits { ..snip.. implicit def byteWrapper(x: Byte) = new runtime.RichByte(x) implicit def shortWrapper(x: Short) = new runtime.RichShort(x) implicit def intWrapper(x: Int) = new runtime.RichInt(x) implicit def charWrapper(c: Char) = new runtime.RichChar(c) implicit def longWrapper(x: Long) = new runtime.RichLong(x) implicit def floatWrapper(x: Float) = new runtime.RichFloat(x) implicit def doubleWrapper(x: Double) = new runtime.RichDouble(x) implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x) ..snip.. }
同じ種類の型変換の暗黙の関数定義があると暗黙の型変換を自動的に行う際にエラーになる。
scala> implicit def intWrap(a: Int) = new runtime.RichInt(a) intWrap: (a: Int)scala.runtime.RichInt scala> 1 to 5 <console>:9: error: type mismatch; found : Int(1) required: ?{val to(x$1: ?>: Int(5) <: Any): ?} Note that implicit conversions are not applicable because they are ambiguous: both method intWrapper in class LowPriorityImplicits of type (x: Int)scala.runt ime.RichInt and method intWrap in object $iw of type (a: Int)scala.runtime.RichInt are possible conversion functions from Int(1) to ?{val to(x$1: ?>: Int(5) <: An y): ?} 1 to 5 ^
まとめ
- 暗黙の型変換用の関数は「implicit」をつけて定義する。
- 使われる変換関数は自動的に最適な型が選択され変換される。
- 同じ種類の型変換関数が複数ある場合はエラーになる。
- 型変換対照を探すのは同じスコープ内に限定される。
- 暗黙の型変換を使うとそのクラスに無いメソッドを呼び出せるようになって便利な反面、乱用するとわけがわからなくなりそう。
*1:どこでどう型変換されているのかがわからなくなるため
パターンマッチ (基本のメモ)
- パターンマッチは条件分岐を記述する switch 文に似た構文
- 主にデータの比較、分解、抽出の用途で使用する。
- 実行時に該当するパターンが選択肢にない場合は scala.MatchError が発生する。
- switch の default はないが、default と同等のパターンとしてワイルカードを指定できる。
- Scala のパターンマッチには break は不要で上から順番にマッチするまで評価してマッチした後は自動で制御から抜ける。
セレクタ式 match { case パターン1 => 処理1 case パターン2 => 処理2 case ... case _ => 上記バターン以外の処理 }
- パターンに該当した後は => の後の処理が順番に評価される。
- パターンに記述できるものが C や java の switch 文よりもはるかに柔軟で豊富
パターンの種類 | 記述例 | 内容 |
---|---|---|
ワイルドカード | case _ | あらゆるオブジェクトにマッチ |
リテラル(整数) | case 1 | 整数 1 であればマッチ |
リテラル(文字列) | case "foo" | 文字列 "foo" であれば |
定数(空のリスト) *1 | case Nil | 空リストだけにマッチ |
変数 *2 | case v | すべてにマッチ。結果を変数 v に束縛する |
型 | case v: Long | Long型であれば |
コンストラクタ | case Foo(1, n) *3 | case クラスのオブジェクトがマッチ。さらにコンストラクタ引数のマッチ |
タプル | case (a, b) | a と b の変数を持つTuple2であれば |
シーケンス | case x :: xs | 先頭要素と残りリスト要素があれば |
パターンガード | case (a, b) if (a % 2) == 0 | 細かい条件分岐を行う場合はガード条件(ifの条件)を付ける |
case クラスの定義
- class 宣言の前に case を付ける。
- case クラスはフィールド宣言の必要がない。コンストラクタの引数が自動的にフィールドとして扱われる。
- toString, hashCode, equals, copy メソッドはコンパイラが自動で実装を追加してくれる。
- Scala はクラス本体が空であれば中括弧({ })を省略可能
scala> case class Foo(a: Int, b: Long) defined class Foo scala> val v = Foo(1, 2L) v: Foo = Foo(1,2) scala> val v2 = v.copy(a = 3) v2: Foo = Foo(3,2) scala> v == v2 res0: Boolean = false
Option#getOrElse
前に勉強用に書いたXMLファイルの要素と属性の表示「XMLファイルの要素と属性をベタに表示」はXMLファイルにXML宣言がない場合は処理できなかったので、Opttion#getOrElse でXMLファイルにXML宣言がない場合は、デフォルトのエンコーディングに UTF-8 を指定するように変更しました。
encoding メソッドはXMLファイルからエンコーディングを取得して Some を返しますが、XMLファイルからエンコーディングが読めない場合は None を返します。
encoding から返された Option[String] が None かを isEmpty で判断してたのですが、Option#getOrElse を使用して None の場合 "UTF-8" をデフォルトのエンコーディングに指定するように変更しました。
(変更前)
..snip.. def main(args: Array[String]): Unit = { val enc = encoding(args(0)) if (enc.isEmpty) { println("not XML file : " + args(0)) return } println("encoding = " + enc.get) val f = new java.io.File(args(0)) val s = Source.fromFile(f, enc.get) ..snip..
(変更後)
..snip.. def main(args: Array[String]): Unit = { val f = new java.io.File(args(0)) if (!f.exists) { println("none file : " + args(0)) return } val enc = encoding(args(0)).getOrElse("UTF-8") println("encoding = " + enc) val s = Source.fromFile(f, enc) ..snip..
Optoin には None の場合に引数に指定したオブジェクトを返してくれるメソッドがあったのですね。
似ているメソッドに orElse があるが Option[T] 型を扱う必要がない場合は、 orElse ではなく getOrElse ですね。
Option#getOrElse は None の場合に指定した v を返しますが Option#orElse は None の場合に指定した Some(v) を返します。
XMLファイルの要素と属性をベタに表示
XMLファイルを読み込むプログラムを勉強がてらつくりました。
Scala は XML 操作が便利。
scala.xml.parsing.ConstructingParser に Source を渡すとXML操作用の Document が取得できるので Node を使って要素のラベルと値、属性、子要素をすべてベタに表示。
import scala.collection.mutable.Seq import scala.io.Source import scala.io.BufferedSource import scala.xml._ import scala.xml.parsing.ConstructingParser object XmlFlatReader { def main(args: Array[String]): Unit = { val enc = encoding(args(0)) if (enc.isEmpty) { println("not XML file : " + args(0)) return } println("encoding = " + enc.get) val f = new java.io.File(args(0)) val s = Source.fromFile(f, enc.get) using[Unit](s) { s => val doc = ConstructingParser.fromSource(s, false).document() statement(doc.docElem) } } def statement(node: Node) { val elems = node \ "_" println(node.label + (if (!elems.isEmpty || node.text.isEmpty) "" else " : " + node.text)) val attrmap = node.attributes.asAttrMap attrmap.foreach { pa => println(" attr : " + pa._1 + " = " + pa._2) } elems.foreach { statement } } def encoding(fname: String): Option[String] = { val src = Source.fromFile(fname) using[Option[String]](src) { src => for (line <- src.getLines) { if (!line.contains("<?xml") || !line.contains("?>")) return None val items = line.split(" ") for (str <- items) { if (str.contains("encoding")) { val ent = str.split("=") return Some(ent(1).replace("\"", "").trim) } } } None } } def using[T](s: Source)(f: Source => T): T = { try f(s) finally s.asInstanceOf[BufferedSource].close } }
Node は子要素がある場合、text に子要素の値をすべて持っているので、自己の値のtext だけを表示してます。
ConstructingParser は XML の encoding を自動で識別してくれないのでencoding の取得のためだけに一旦、XMLファイルを読み込んでXML宣言にある encoding を取得。
もうちょっと良い方法があるのかもしれないが、とりあえず encoding が取れて指定できました。
XML宣言がない XMLファイルは読めないので XML宣言がない時は決め打ちで、UTF-8 を指定してもよかったかもしれない。
Source は close がないのでローンパターン *1 で BufferedSource にキャストして close。using に渡すクロージャの戻り値の型を型パラメータで指定してます。
Map で foreach すると キーと値の Pair *2 を渡してくれるのですね。
\ は NodeSeq のメソッドで、指定タグの要素を返してくれて便利。
ここでは使っていないが \\ はさらにネストした要素をサーチしてくれて、指定する名前の先頭に @ を付けると属性をサーチできるようだ。
"_" を指定したときは全ての子要素の集合を返してくれます。これで全要素を取得。
要素の Node パース用の statement メソッドは末尾再帰になっていないですが、末尾再帰関数にする良い方法が他にあるのかもしれません。