Scalaな日
某所で Scala By Example (Scala 本家のドキュメントページから取ってこれます。こちらに和訳プロジェクトがありますが正直まだ品質は微妙です) の勉強会しましたという記録だけ。
Generic Type, co-variant/contra-variant と Type Parameter Bounds
ここは私の担当部分ではないのですが、あまり関数型の型理論に詳しい人間がいなかったせいか、どうも理解が……。というか Scala by Example の説明も悪いと思う(と人のせいにする)。
ぶっちゃけ全然わかんなかったのが co-variant と contra-variant (「共変」「反変」という訳語を見かけたけど???だったり)。
例えば T が S の subtype の場合、stack[T] は stack[S] の subtype になるよね、こういう関係を co-variant と呼びます。うん、これは分かる。
んで、クラス stack に対して
class Stack [+A] {
みたいな宣言があったら、この "+" は covariant な subtype だという意味だよと。うん、これも分かる。
でも、Scala では "-" のプレフィックスもあって、こいつは contra-variant な関係を示しますよ、contra-variant っていうのは T が S のsubtype の場合、stack[S] が stack[T] の subtype になる関係だよ、っていきなり言われましても……。
その後の Lower Bounds は言語として意図してることがなんとなく分かったので、こいつを導入するための踏み台なのかなーと解釈してその場は逃げたんですが、下に kmizushima さんに丁寧な解説をいただいている(本当にありがとうございます!)とおり、ちゃんと意味があるよ、ということなんで、もちょっとちゃんと読みましょう。ページにして56ページあたりです。
えっと、まず、「純粋な関数型の世界では、全部の型は co-variant だよ」純粋ってことは破壊代入を許さない世界ってことだよね、ふむふむ、「でも、mutable (書き換え可能) なデータを導入すると事情が変わるよ」えー、なんでなんで?「まあともかくこんなクラスを考えてみようよ」
class Array[A] { def apply(index: Int): A def update(index: Int, elem: A) }
JavaなんかだとArrayはcovariantだから、Javaと同じ型システムだとすると、例えばこんな感じで実行すると、
val x = new Array[String](1) val y: Array[Any] = x y(0) = new Rational(1, 2) // y(0) は y.update(0, new Rational(1, 2)) の糖衣構文
1行目はいいよね。2行目は、StringはAnyのSubtypeだから、covariantにおいてはこの代入はOKになるよね。三行目も、RationalはAnyのSubtypeだから問題ない……あれ?
で、Javaはこれを値を保存するときに実際の型チェックをやって、StringにはRatinalは入れられないよ! と例外を吐く。でもそうじゃなくて、コンパイルエラーで止めたい。どうするか。
そこで、さっきの2行目に着目。Anyに代入できちゃうのはあんまりにも強力すぎるだろと。ということでScalaはJavaと違ってArrayはcovariantじゃなくてnon-variantがデフォルト。型修飾子をつけなければ型を変えられない。でもこれって縛りがきつすぎるよね。どうしよう?
ということでScalaではクラスの型パラメータの持つ意味づけを少し変えて、原文の言い方をそのまま使うと「covariantな型パラメータはクラス内のcovariantな位置だけに出てくる」。雑駁な理解では、メソッドの戻り値としての型は covariant だけど、メソッドのパラメータは covariant とは限らないでしょう? ってことみたい。
つまりさっきの定義で、
class Array[+A] { def apply(index: Int): A // <- ここはOK def update(index: Int, elem: A) // <- ここは、Aよりも「狭い」型(=contra-variant) じゃないと駄目 }
ということ。
ああ、やっと意味が分かった気がするぞい。
つまり kmizushima さんの指摘どおりで、クラスの型パラメータを、メソッドの戻り値として考える場合と、自己書き換えとかに使う場合では、許される継承関係が逆になるでしょう? ということですね。
なんか納得した。
List と For Comprehension
List 処理は平凡なのでさらっとスルー。強いて言えば :: で cons セルを扱う文法って結構関数型ではメジャーなのかね。car / cdr / cons にそれぞれ関数を割り当てるのって Lisp ぐらいなものなのかしら。
んで、このエントリが仮だったときに、
普通に List Comprehension のスタイルでかければいいような気がするんだけどなんで For 文っぽく書くようにしたのかね。やっぱり「関数型とかわかんないよ」な人に書かせるための言語なのかしらん。
と書いたら、「それは違います」とご丁寧に突っ込んでいただきました。
これは kmizushima さんのあげていただいた例をそのままいただくとしましょう。
(for(v1 <- map.get(k1); v2 <- map.get(k2); ...) yield 成功したときに返す値).getOrElse(失敗したときに返す値)
yield でさらに値を洗える(と言う言い方が適当かどうか分からないけど)が List Comprehension に対する For Comprehension の強みであると。
ああ、そういうことなら納得。
でも Scala By Example にはそういう例は一個も出てこなかったぞ。うむー。
あと For Comprehension は
def map[B](f: A => B): C[B] def flatMap[B](f: A => C[B]): C[B] def filter(p: A => Boolean): C[A]
の関係が成り立つ C なら基本的には定義できるので trait で定義できたらうれしいんだけど、Scala の型システムはちょっとそれを保証するには弱いからできないとかなんとか言い訳っぽくかいてありましたが、実はちゃんと読んでません (^^;)
Stream
無限リスト的なことを定義できます。
ただし、「どこを取り出すか」という評価そのものに filter を使ってしまうと無限に評価が走って帰ってこないです (当たり前ですが、これも kmizushima さんにご指摘いただくまでわかりませんでした。感謝感謝)。takeTo とかで外部からどこを取り出すか指定するか、どうしても評価でどの値を取り出すか決めたい時には iterator を使いなさい、ということなのでしょう*1。
勉強会でご一緒いただいた皆様に感謝するとともに、素晴らしいコメントをくださった kmizushima さんにあらためて感謝を。
私のエントリよりずっと有用なのでぜひコメントも読んでくださいね。
*1:これも kmizushima さんより takeWhile を使うといいよ、という助言をいただきました。ぜひコメント欄をごらんください。コード例入りで丁寧に解説してあります。