Scalaに入門してみた その最後
Scala入門シリーズその最後です。入門なので、ライブラリとかは一切合切無視して進んでいます。その辺りはご了承下さい。
今回はScalaの特徴的な部分に対する入門です。
trait
// 基本的なtraitの定義 trait Hoge { println("init Hoge") def hogehoge = println("hogehoge") } trait Huga { println("init Huga") def hugahuge = println("hugahuga") } class Parent { def name(n:String) = println(n) } // 親クラスを継承して、traitをミックスインする。 class Child extends Parent with Hoge { override def name(n:String) = println("child name is " + n) } (new Child).hogehoge -> "hogehoge" // インスタンス化する際に、traitをミックスインすることができる (new Parent with trait).hogehoge -> "hogehoge" // インスタンスに対してtraitをミックスインしても、別のインスタンスには影響しない (new Parent).hogehoge -> エラー // 複数のtraitをミックスインすることもできる。コンストラクタの順番は、ミックスインで指定した順番 (new Parent with Hoge with Huga) -> init Hoge init Huga
traitは、インターフェースに実装を持たせるようにしたもの、という感じです。Javaのinterface同様に、複数のtraitを継承、ではなくてミックスインすることができます。このあたりはRubyのクラスとかに影響されたんでしょうか。ミックスインと聞くとそれしか思い浮かびませんが。
実装を持てるので、Javaのinterfaceであった、「一部では別の実装だけど、大多数は同様の実装」になるような場合に、実装クラスで同様の実装を書かなければならない、といった場合に、traitにデフォルト実装を持てることから、実装を簡潔にすることができる、といった感じです。
traitは実装を持つ以上、interfaceとは違って、「同一メソッドについてどのtraitの実装を使うのか?」という問題に対して、次のようなやりかたを用意しています。
trait Hoge { def hogehoge = println("hogehoge") } trait Huga { def hogehoge = println("hugahuga") } // superは、基本的に「extends/withの最右」のクラス/traitが使われる class Foo extends Hoge with Huga { override def hogehoge = super.hogehoge } (new Foo).hogehoge -> hugahuga // 同じインターフェースを持つtraitのうち、実際に利用するtraitを指定する class Foobar extends Hoge with Huga { override def hogehoge = super[Hoge].hogehoge } (new Foobar).hogehoge -> hogehoge
superというキーワードは、そのクラスのスーパークラス/traitを表しますが、Scalaでは、このsuperがどのクラス/traitを指すか、というのは、線形化という処理で決まるらしいですが、まぁそこまで深く考えないとならない事態にはあまりならないような予感がしますが・・・。
もっとtrait
traitはどうもScalaのかなり深い部分を占める機構らしく、まだ機能があります。
// trait単体をインスタンス化できる trait Hoge { def hogehoge = println("hogehoge") } (new Hoge).hogehoge -> hogehoge trait Huga { def huga def hogehoge = println("hugahuga") } // 定義を書かなければそのまま抽象メソッドになる。Javaのinterfaceと同様 trait Worker { def work def talk = println("talk") } new Worker // インスタンス化はできない val a = new Worker { def work = println("I'm working!") } // インスタンス化する際に定義を与えられる a.work -> "I'm working!" a.talk -> "talk" // traitを重ねることで動作を変更できる abstract class Person { println("Person constructed") def work(time:Int) } class Normal extends Person { println("Normal constructed") override def work(time:Int) { println("Normal : work start") for (item <- Range(0, 60, time)) { println("take rest : " + item) } println("Normal : work end") } } // abstract override することで、そのメソッドの中で、抽象メソッドを呼べる // また、extendsすると、そのクラスかそのクラスの派生クラスにしかミックスインできないように制限できる trait Newtype extends Person { println("Newtype constructed") abstract override def work(time:Int) { println("Newtype : work start") super.work(time * 3) println("Newtype : work end") } } (new Normal).work(3) Person constructed Normal constructed Normal : work start take rest : 0 take rest : 3 take rest : 6 take rest : 9 take rest : 12 take rest : 15 take rest : 18 take rest : 21 take rest : 24 take rest : 27 take rest : 30 take rest : 33 take rest : 36 take rest : 39 take rest : 42 take rest : 45 take rest : 48 take rest : 51 take rest : 54 take rest : 57 Normal : work end // インスタンス時にミックスインすることで、動作を変更することができる (new Normal with Newtype).work(3) Person constructed Normal constructed Newtype constructed Newtype : work start Normal : work start take rest : 0 take rest : 9 take rest : 18 take rest : 27 take rest : 36 take rest : 45 take rest : 54 Normal : work end Newtype : work end
こういう形の処理を、「traitを積み重ねる」と表現するらしいです。表現としては、ミックスイン先のクラスにあったメソッドのdecoratorみたいなもんでしょうか。decoratorにdecoratorを足してやる、という表現ができそうです。それを言語機能にとりこんだようなもんでしょか。OCamlだとちょうどFunctorっぽい?
型パラメータ
// 型パラメータは関数名の後に[]で囲んで、カンマ区切りで指定する。 // 利用時に指定しなければ型推論が行われる。型を指定したい場合は関数名[型名]()とする def test[A](n:A) :A = n test(1) -> 1 test[String](1) -> Error // もし同じ型パラメータに対して複数の型が渡され、かつ型パラメータを型推論にまかせると、 // 全てAny型として扱われる def test[A](n:A, m:A) = (n,m) test(1, "string") -> (1, string) // classでの型パラメータの指定方法も関数の場合と特に変化無し class Wrap[A] { var param:A = _ def get:A = def set(p:A) = this.param = p } val w = new Wrap[String] // 型パラメータを指定しないままだと、Aに対してNothingという型が割り当てられる。 val w = new Wrap // 型パラメータに対する境界制限 // 上限境界。ある型パラメータは、EA1もしくはのサブクラスしか設定できない class Base class EA1 extends Base class EA2 extends EA1 class Test[A <: EA1] new Test[Base] -> Error new Test[EA1] -> OK new Test[EA2] -> OK // 下限境界。ある型パラメータは、EA1もしくはEA1のスーパクラスしか指定できない class Test2[A >: EA1] class Test3[EA1 >: A <: EA1] // クラス定義を指定する。これは部分型という表現になる。 class Test4[A <: { def temp():Unit}]
OCamlでいう型変数です。まぁ使い方とかはそのまんまですが、こちらはオブジェクト指向ががっつり入ってきている関係で、境界制限というものがあります。通常の型変数では、基本的にどんな型でも型変数に入りますが、それを特定の継承ツリーだけにしてしまえ、という感覚でしょうか。
また、この型変数に対する、変位指定というものがさらにあります。これは、同じクラスだけども別の型が入っている変数に対して代入するときなどに働くようです。変位については以下の通りです。
非変 | def test[A] |
共変 | def test[+A] |
反変 | def test[-A] |
共変の場合、その型変数に適用されている型の「サブタイプ」なら許可されます。反変はその逆で、「スーパータイプ」なら許可されます。
どちらかというと、JavaのGenericsの発展形、という感じがします。そしてOCamlとかHaskellの型変数とはどこか違う感じが漂いますが、私にはどの辺がどう違うのか、というのを表せるほど知識も無いので放っときます。
暗黙の型変換と抽象型
class LongLongNameClass { def exec = println("exec") } // typeキーワードに指定した型で、元の型の名前を別名で扱うこともできる。 class T { type S = LongLongNameClass def test(s:S) = s.exec } val x = new T x.test(new LongLongNameClass) // -> exec // 関数の前にimplicitを付けると、Aという型からBという型への変換が必要になった時点で // 暗黙的にその関数が利用される。 implicit def intToString(num:Int):String = num.toString val str:String = 10 // -> "10" class X class Y // 可視境界。Xのサブタイプか、暗黙の型変換を通してXにできる型ならば型パラメータに設定できる。 class Z[A <% X] new Z(new X) // -> OK new Z(new Y) // -> NG implicit def yToX(y:Y):X = new X new Z(new Y) // -> OK // Generalized Type Constraints(型パラメータの制約) // AがIntの場合のみ、workを呼び出すことができる。 class Worker[A] { def work(implicit t:A =:= Int):Unit = println(t.toString) } val i = new Worker[Int] // -> OK val s = new Worker[String] // -> OK i.work(10) // -> "10" s.work("hoge") // -> Error // 具体化された型パラメータがBまたはそのサブクラスの場合 class Z[A] { def test(implicit t:A <:< B) = println("test") } // 具体化された型パラメータが、Bから暗黙の型変換で変換できる場合 class Z[A] { def test(implicit t:A => B) = println("test") }
抽象型は、実際には抽象クラスから派生したクラスでその型を具体化して利用する、というようなもののようですが、長い型名だったりなんだりを別名として扱うことができるようです。
そして暗黙の型変換ですが、こいつはつまるところ、CとかでIntをDoubleに入れてもなんとかなる、というものを明示的に行うための機構のようです。暗黙の型変換にはルールがあり、
- implicitが付与された定義のみが使用される
- 暗黙型変換関数は、単一の識別子としてスコープ内にあること
- 要は、同一スコープ内になければ使われないってことです
- 暗黙変換は一度しか実行されない
- 同じ変換が行える関数が二つ以上存在するとエラー
- 型のチェックで問題無ければ行われない
というルールがあるようです。
・・・なお、本当はこれ以外にも暗黙の引数などもあるようなのですが、がっつり飛ばしました。理由は、それを使う意義がさっぱりわからんかったからです。引数としてその時点で定義されている変数が自動的に使われるとか、そんなちょっとの部分くらい明示的にやったっていいじゃないですか、という感じ。間違ってるかもしれませんが。
Generalized Type Constraintsは、メソッドに対する型パラメータの制約です。classとは独立しているので、classは作れるけども、このメソッドは特定条件の場合だけしか呼ばせない、など、多分本来はclassの境界条件などと併せて使うんじゃまいか、と思っています。
そしてここまできて、たまにScalaでみる =:= とかいう謎の記号がなんだったのかがわかりました。ぱっと見は眉間に皺寄せてる人ですね。
簡単な入門を終えて
ひとまずこれでScalaに対する簡単な入門は終了です。一通りやってみて思いましたが、やっぱりScalaは「オブジェクト指向言語」なのだなぁ、と。OCamlとかHaskellとかとは、やっぱり根本の思想が違うため、感覚は全く違うものとなりそうです。
ですが、その中でも関数型言語らしく、var と valの区別とつけていたり、関数がファーストクラスであったりとして、Javaばっかりやってきた人でも、とりあえずはJavaと同じに書いても動きますし、その中で関数型言語のエッセンスを使っていける、というのが魅力なのかなぁ、と。
暗黙的な型変換については、私はCとかで嫌な思い出しか無いのでちょっと否定的ですし、下手するとわけのわからんバグの元となったりするらしいので、よほどのことがない限りは使わなくてもいいんじゃないかなぁ、と。せっかく型安全にできるんですから、そうやっていくというものいいんではないかと。
Scalaには優秀なフレームワークや、より優れた平行処理のフレームワークもあったりしますが、とりあえず私の勉強はここで一旦終わりにします。