ちょっとだけScala (その1)
某クローズドな勉強会でScalaやるんだけど来ます? と言われて行きます行きます! と言ったのは昨年の12月でありました。
内容はプログラミング言語 Scala Wikiの「Scala by Example」を輪読しながらあーだこーだツッコミを入れるという内容。前回は5章までやったのか。
んで、次回は今月なんですが、みなさんやりたいとこあります? と聞かれて即挙手したのが7章の「Case Class and Pattern Matching」。
Prolog 屋のぼくとしては Erlang や Haskell のパターンマッチは単純に便利機能であって関数型の本質とはなんら関係がないと理解してるんですが*1、そこら辺になんか答えがあるかなーと思ってね。
ということでちょっとだけ Scala やってみたので感想とかつまったところとか……。
Scala そのもののファーストインプレッション
大体が Scala って名前をちょろちょろ耳にし出したのは Twitter が Rails 遅いからって Scala に乗り換えた(超訳)って話が出たあたりかな。
んで誰だったかに「Scala って何がポイントなの?」と聞いたら「Java VM で早いからじゃないです?」って言われてしょぼーん。ぼくはサンデープログラマなんだから速度とかスケーラビリティとかそういう実用面はどうでもいいの! 言語仕様が萌えられればいいの!
ということで、やること多すぎって理由もあって超放置してたんですが、一方でぼくの中でずっとあった疑問。
関数型言語のうれしさというのはなかなか明解に解いてくれる人がいないのだけど、少なくとも Joe Armstrong が「Erlang プログラミング」で書いていた「変数の破壊代入をしないことで副作用のないコードが書けることが大きなメリット」ということはあると思う。
一方で大抵のオブジェクト指向の場合はオブジェクトの内部に状態を持つ。これは破壊代入なくしては実現できないことで (誤解があったら教えてください)、したがって「オブジェクト指向関数型言語」というのが成立するとしたらどういうものであるか、が知りたいわけなのです*2。
なーんてことをあちらこちらで言っていたら、「いや、Scala はそれをちゃんとやってるらしいよ」って言われて、ほほう、と思ったわけ。
んで、結論。
ふつーに破壊代入もある、ふつーのオブジェクト指向でした。
つまんねー。
あと Scala というと Static Type System への回帰ってのが言われてますね。
Ruby や Smalltalk *3 や
Haskell やPython や*4 Scheme なんかの Dynamic Type だと書くのはラクかもしれないけど間違ったときの検出が大変じゃん、やっぱり事前にある程度チェックできないと。
え、型いちいち書くの大変? でも大丈夫、型推論あるからサボれるよ。
とかなんとか売り文句になってますが。
えーと、この型推論、あんまり記述を楽にするように思えないんですが……。まあ、まだそこまでちゃんと勉強してないけど、結局メソッドの引数や戻り値は全部明示的に型指定しなきゃいけないし、ダメダメじゃないですか……。
つことでぶっちゃけ、「あーもういいや」って感じ。勉強会終わったら封印だな。
ただし実用言語としてはいいと思います。
あえて政治的に正しくない言い方をすると「奴隷言語」なんですな、これは。
つまり、コアロジックで関数型で書くとスマートに書けるようなところは、優秀な奴がぱっと書いちゃう。そうじゃなくて人月でねじ伏せちゃうようなコードは、今まで Java でなんとかかんとか仕事してた人をたくさん捕まえてきてちょっと仕込めば手続的に書けてしまう。そういう人たちにちゃんとしたコードを書かせるにはやっぱ静的型システム必要。
というのがぼくの理解ですが、間違ってたらツッコミしてね。
ちょっとだけScala
ということでぶつくさいいつつ、勉強会はちゃんとやらねばならんので読みました。7章。
Case Class
Case Class ってのはなかなか面白くて
abstract class Expr case class Number(n: Integer) extends Expr case class Sum(e1: Expr, e2: Expr) extends Expr
とか書くと、次のようなクラスに勝手になると。
- コンストラクタを暗黙的に生成してくれる。だから new とか書かなくていい。*5
- toString, equals, (その副産物として) hashCode を用意してくれる
- アクセサも用意してくれる
- 後述のパターンマッチングに使える構造にしてくれる
上の例だと abstruct class Expr から extends してるけど、なくてもいいらしくて、この場合はスーパークラスは Any になるらしい。
もちろんそれ以外のメソッドも作れるけど説明面倒くさいので省略。
Pattern Match
さくっと例だけ見せて。あ、Expr, Number, Sum の定義はさっきのとおり。
def eval(e: Expr): Int = e match { case Number(n) => n case Sum(l, r) => eval(l) + eval(r) }
ミソは e match { case p1 => e1 ... case pn => en } という構文ですね。p (パターン) に何をとれるかというのは他所を参照していただくとして、この場合だと e: Expr としているのでこの二つ以外ありえないのですが、そうじゃない場合は無名変数 _ を else 的に使えるみたいです。
あと今回のテキストでは1バイトも触れられていませんが正規表現でのマッチもできるらしいですね。
しかし一般的なオブジェクト指向では「型見て switch するようなコード書いたら負け、ポリモルフィズム使えよ」という風に言われてるんですが、Programming Scalaにはどっかに「オブジェクト指向っぽいのがいいならポリモルフィズムだし、関数型がお好みならパターンマッチじゃね?」みたいなことが書いてあったような気がしますが見つけられませんでした (^^;)
あ、もちろん eval をクラスの中に入れて、こんな風にもかけるよ、とのことです。……けどこれ、Number や Sum の定義とどっちを先に書けばいいんだ? まあいいや。
abstract class Expr { def eval: Int = this match { case Number(n) => n case Sum(e1, e2) => e1.eval + e2.eval } }
演習問題を解いてみた。
まあそんなわけで演習問題がでてたので解いてみた。
Exercise 7.2.2 Consider the following definitions representing trees of integers.
These definitions can be seen as an alternative representation of IntSet:abstract class IntTree case object EmptyTree extends IntTree case class Node(elem: Int, left: IntTree, right: IntTree) extends IntTreeComplete the following implementations of function contains and insert for IntTree’s.
def contains(t: IntTree, v: Int): Boolean = t match { ... ... } def insert(t: IntTree, v: Int): IntTree = t match { ... ... }
訳すまでもないので訳は省略。
こういうデータ構造の場合は再帰でやるに決まっているのでProlog的に書けるから得意だぜ。へへん。と自信満々に書いたら動かなかった。最初の解答はこう。
abstract class IntTree case object EmptyTree extends IntTree case class Node(elem: Int, left: IntTree, right: IntTree) extends IntTree def contains(t: IntTree, v: Int): Boolean = t match { case EmptyTree => false case Node(elem, left, right) => (elem == v) || contains(left, v) || contains(right, v) } def insert(t: IntTree, v: Int): IntTree = t match { case EmptyTree => new Node(v, EmptyTree, EmptyTree) case Node(elem, left, right) => if (v == elem) t else if (v < elem) (if (contains(left,v)) left else Node(elem, insert(left, v), right)) else (if (contains(right,v)) right else Node(elem, left, insert(right, v))) }
ホントは insert はこう↓書きたかったんだけどダメだった。なんか勘違いしてるのかなあ。
def insert(t: IntTree, v: Int): IntTree = t match { case EmptyTree => new Node(v, EmptyTree, EmptyTree) case Node(v, _, _) => t case Node(elem, left, right) => if (v < elem) ... }
まあいいや。とにかくできたので試してみた。
まずハマったのが型について。
Welcome to Scala version 2.7.5final (Java HotSpot(TM) Server VM, Java 1.6.0_15). Type in expressions to have them evaluated. Type :help for more information. scala> :load ex.scala Loading ex.scala... defined class IntTree defined module EmptyTree defined class Node contains: (IntTree,Int)Boolean insert: (IntTree,Int)IntTree scala> var t = EmptyTree t: object EmptyTree = EmptyTree scala> t = insert(t,10) <console>:11: error: type mismatch; found : IntTree required: object EmptyTree t = insert(t,10) ^
これで二時間悩んだんだから笑わないでね。
正解は、もちろん var t: IntTree = EmptyTree です。
でも型推論っていうなら下位のクラスが代入される変数はそれが extends してる抽象クラスだなーとか思ってくれてもよくない? まあ、どこまで遡ればいいかわかんないってことだと思うんだけど。
さっそく直してみたが、t = Node(n, Node(m, ...), Node(l, ...)) (m < n < l) のときに insert(t, m) するとうまく動かない。あれえっ。
ま、トレースすればすぐ分かるよね、と REPL のヘルプ見たら、
scala> :help Welcome to Scala version 2.7.5final (Java HotSpot(TM) Server VM, Java 1.6.0_15). Type in expressions to have them evaluated. Type :help for more information. Type :load followed by a filename to load a Scala file. Type :replay to reset execution and replay all previous commands. Type :quit to exit the interpreter.
と、トレースとかブレークポイントとかないの……?
しょうがないからぐぐるせんせいに聞いたら Eclipse Plugin 使えと。
あいあい〜〜、Eclipse なんて使ったことないけどやってみるよー。
ということで長くなったので以下次号〜〜〜。
*1:んで、ちょっと前のエントリで Erlang のパターンマッチは右辺にも左辺にも未束縛変数をおいてマッチできるのに向きがありげな記号 -> を使うのは変だと主張したら Erlang は FP で Prolog は LP だから同じに考えるな、というコメントに対して噛みついたわけですがそれはさておき。
*2:んで OCaml どうなの? と聞いたら「ML 系は破壊代入しまくりだから普通に内部状態持ちます」とか言われて萎え萎え。というのは別の話。
*3:厳密にはこの二つは Dynamic Type ではなくて、Basic Type が存在しない、すべての要素が Object クラスからの単一継承なので、オブジェクトの Dynamic Binding と言う方が正しいのかな。
*4:Twitter で「Haskell って Static Type じゃね?」というご指摘をいただきました。ですよねー。なーんで間違えたんだろ。ということで例をScala本でも言及されてる Python に入れ替え。
*5:一番肝心なことを書き忘れていて勉強会で指摘された ^^;