Table of Contents
概要
約30週間、社内で20人ほどでScala勉強会を開催し、通称コップ本を読破しました。
何と言ってもScalaという言語の作者が書いた本です。これが一番いいだろうということで選びました。
結果、脱落者が出るわ出るわ。中盤~最終回は常に2~3人。彼らは後輩なんで、無理して出てくれたのかもしれません。僕に人望が無いのかScalaが難しいのか。恐らく後者だと思います。かなりヘビーで、僕の心も何度も折れかけましたし、途中で抜けたメンバーはみんなこんな風なことを言い残して去りました。
「何が分からないのかも、分からなくなりました」
でも、最後まで勉強会に出てくれる仲間が1人でもいたから、挫折せずになんとか最後まで読めました。僕はラッキーでした。
そんな本の感想文です。勉強会は2016年12月12日(火) ~ 2017年7月10日(月) でした。
Scalaを学ぶ価値はあるか?
僕は間違いなくScalaを学んでよかったです。
この本を読むことで、Scalaの存在意義の本質について理解が深まりました。
- なぜScalaで圧倒的に短いプログラミングが書けるのか、それを可能にしているのはどの文法なのかを、説明できます。
- 関数型言語なら、なぜJava8やjavascriptやHaskellではいけないのか、説明できます。
- DSLとは何なのか、なぜScalaがDSLに適した言語なのかを、説明できます。
これらの用途において圧倒的に優れているから、Scalaを学ぶべきなのです。
どの文法が優れているのか
これらを説明する前に、Scalaの成り立ちを説明したいと思います。
Scalaという言葉は、”Scalable”を意味しています。これは、あらゆるプログラマが文法を追加できるということを意味しています。例えば、以下の文法は言語のビルトインではありません。
1 |
val map = Map(1 -> "one", 2 -> "two") |
ではどうなっているかというと、Predef$.classを見ますと、
1 2 3 4 5 6 7 |
final class ArrowAssoc[A](val __leftOfArrow: A) extends AnyVal { def x = __leftOfArrow @inline def -> [B](y: B): Tuple2[A, B] = Tuple2(__leftOfArrow, y) def →[B](y: B): Tuple2[A, B] = ->(y) } @inline implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x) |
となっており、手作りだということが分かります。さらに、代替でマルチバイト文字→が使えることが分かります。まぁこのコードを理解するには、この本を読破してないと無理なような・・・循環参照が起きている気が・・・
ということは、Scalaは誰かがすごい文法を作り、それが支持を得て、広く使われ定着していくという風に進化してきていると思われます。
すごい文法ベスト5
個人的に感動したのは、
- implicit
- trait
- 部分関数+event処理(swing/actor)
- スレッドに対する抽象演算
- パーサーコンビネーター
です。
なぜその文法が優れているのか
implicitは、文法を大幅に節約できる文法です。まずimplicit defの機能は、暗黙の型変換を行うことです。
1 2 3 4 5 6 7 8 9 |
class NewDouble(origin: Double) implicit def toNewDouble(origin: Double): NewDouble = new NewDouble(origin) // これ import scala.language.implicitConversions // 対話型シェル以外では不要 implicit def toNewDouble(origin: Double): NewDouble = new NewDouble(origin) val hoge: NewDouble = 1.0 // なぜか代入できる scala> hoge res4: NewDouble = NewDouble@5f4df55e |
どうでしょう。これすごくないですか。つまり、自分で書いたNewDoubleライブラリを使うときは以下のコードでいいのです。
1 |
val hoge: NewDouble = 1.0 |
次にimplicit receiverの機能は、既存クラスに暗黙にメソッドを追加することです。
1 2 3 4 5 6 7 8 |
class NewDouble(val origin: Double) { def **(p: NewDouble):NewDouble = new NewDouble(scala.math.pow(origin, p.origin)) } // 1つメソッドを追加 implicit def toNewDouble(origin: Double): NewDouble = new NewDouble(origin) // 上と同じ implicit def toDouble(origin: NewDouble): Double = origin.origin // 追加 val hoge = 2.0 ** 3.0 // hoge.origin == 8となる val fuga:Double = 2.0 ** 3.0 // fuga == 8.0となる |
どうでしょう。2.0や3.0はDoubleというクラスで、**(べき乗)というメソッドをもっていないのですが、
1 |
val fuga:Double = 2.0 ** 3.0 |
と書くと動くのです。Doubleという既存クラスにメソッドを追加してしまいました。
他に
- implicit class
- implicit parameter
があります。このimplicit parameterが素晴らしい。しかし、入門記事として、そろそろ読んでくれる人がいなくなりそうなので止めておきます。いや、やっぱ一言だけ言わせて。
1 2 3 4 5 6 |
val arr = Array(3, 1, 4, 1, 5, 9, 2) scala.util.Sorting.quickSort(arr) arr res20: Array[Int] = Array(1, 1, 2, 3, 4, 5, 9) |
どうでしょう・・・。Comparatorを書いてないのにソートされてます。なんでやねん。implicitだから書かなくていいのです。
結論として、僕はこんなにコードが節約できる言語を知りませんでした。
implicitの解説は第21章にあります。
ちょっと、このまま書くと軽く5倍の長さになりそうなんで、2位~5位については、需要ありそうだったらまた解説します。
なぜこの本は難しいのか
観察によれば、
- この本は後ろに行けば行くほど「分からない」と主張する人が増える
- Scalaの本質ではないにもかかわらず、関数とくにmap/filterで躓く人が多い
- しかし、foreachがわからないという人はほぼいない
- だが、初見でfor式を真に理解できる人もほとんどいない
- Scalaの本質を理解するにはソースコードを読む必要があるが、理解不能な高度なScalaで書かれており読めない
まず問題1.については、「全ての章を読むのを諦める + 空いた時間で手を動かす」のが解決策だろうと思います。この本は700ページありますが、3倍くらいに圧縮されて書かれており(作者の頭の回転が速すぎるため。)、2000ページぐらいのボリューム感を感じます。実際に理解するにはほとんどのコードをインタプリタに打ち込む必要があり、膨大な時間を要します。
具体的には、一周目でコードを打ち込まずに理解できるのは1章~4章まで、つまり最初の100ページまでが限界だと思われます。4章が終わった時点で、テキストエディタで書いた.scalaファイルからCLIアプリケーションを1からビルドできる力が身についていないと、5章以降のコードが動かせず、ほぼ理解できません。
次に問題2~4です。Scalaが全てjvmに翻訳される限り、(上級)JavaプログラマーがScalaを理解できないということはあり得ないので、本書を別の方法で攻めてみて、Javaとして理解することも手としてはありなのかもしれません。
例えば、mapに躓いたら、trait Traversableを読んでみるとか。
1 2 3 4 5 6 7 8 9 10 |
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { def builder = { // extracted to keep method size under 35 bytes, so that it can be JIT-inlined val b = bf(repr) b.sizeHint(this) b } val b = builder for (x <- this) b += f(x) b.result } |
このように、よくわかるJava的なScalaで書かれております。(implicit parameterが使われてますがw)
先にHaskellを学んだ方がいいのか
もしくは、関数型プログラミングを先に学んで(haskellが簡単でよいです)、純粋関数・参照透明性・Monadあたりを勉強しておき、躓かないようにすることが考えられます。for式で違和感を感じても、これってモナドじゃね?とピンとくると思います。
「すごいH本」がデファクトスタンダード。一番簡単なのがいい人はふつうのHaskellとか、ちゃんと理解したい人はReal Worldとか。ガチ勢はSchool of Expressionとかどうでしょうか。
最後に問題5ですが、この記事を読んで意味不明と思われている方には共感していただけると思います。この対策としては、この本を2周以上読むしかありませんので一緒に頑張りましょう。
「100回読めば、わかる」(コンクリート屋だった祖父 – 誰よりも強く横暴で賢かった – の幼い僕への説教)
“Work hard.”(Deep Learningをよりよく理解する方法を尋ねられた、CaffeのLead開発者Evan Shelhamerが質問者に言い放った言葉)
じゃあなぜHaskellではダメなのか
これは僕の大好きなSwingを例にとるとわかりやすいと思います。scala.swingでは、部分関数がawtのイベントリスナーにimplicit変換されます。
例えば、テキストボックスにフォーカスしたとき全選択を行うコードは
1 2 3 |
reactions += { case e: FocusGained => this.selectAll } |
となります。
なんて美しいのでしょうか。Haskellでこのように美しくswingを書くことは出来ません。
もちろん、GUIをやるのにswingを書く必要が無いのはわかっています。しかし、それは本質ではありません。本質は、全てのJavaライブラリが、implicit conversionによって非常に短い文法で利用できることなのだと思います。
DSLとは何でありなぜScalaがよいのか
DSLとはDomain Specific Languageつまりオレオレ言語であります。別にどんなライブラリでも「言語だ」と言い張ることは出来ますが、
- 自然言語に見える もしくは
- +や-、最初の例の→など、誰でも知っている演算子を再利用している
- 言語のパーツは状態を持たない
- 状態を集約するimplicit Context的なものがある
などが満たされていないと、誰にも使われないでしょう。Scalaはまさにこれらをやりやすくすると言えます。
DSLの目的はオレオレ言語→Scala/Javaプログラムという変換をすることです。例えばshouldというオレオレ言語がscalatestにあります。
1 |
"A Stack (when empty)" should "be empty" in { ...(テストの中身)... } |
のように使います。このソースコードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 |
trait StringShouldWrapperForVerb { val leftSideString: String val pos: source.Position def should(right: String)(implicit svsi: StringVerbStringInvocation): ResultOfStringPassedToVerb = { svsi(leftSideString, "should", right, pos) } } |
先ほどの要点を確認してみると、
- shouldはleftとrightを利用して自然言語に見えるようにしている
- 演算子は今回は登場しない。{}はおなじみである
- should自体はsvsiを呼ぶだけで、まったく状態を持たない
- sourceというcontextがある
のようになっていると思います。
人がいない問題はどう解決されるか
時間が解決します。
本当にいいものは、歴史に残ります。で、悪いものは歴史から消えていきます。
僕が約10年前、サルガッソーという会社で、おぼつかないながらも初めてjavascriptを書いていた時、jQueryが出たばかりで、どっちかというとprototype.js全盛でした。やがてprototype.jsどころかprototypeごとどこかに消えました。そして、ちょうどそのころ、jsdefferedが出ました。2009年。僕の師匠Fさんが教えてくれたのを思い出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
http = {} http.get = function (uri) { var deferred = new Deferred(); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status == 200) { deferred.call(xhr); } else { deferred.fail(xhr); } } }; deferred.canceller = function () { xhr.abort() }; return deferred; } |
https://cho45.stfuawsc.com/jsdeferred/doc/intro.html
これがcho45さんのホームページです。やっぱりよくわかりません。Deferredオブジェクトってなんやねんっていう。
これはjQueryにマージされて、もっとまったりとしたものになりました。
https://api.jquery.com/deferred.then/
1 2 3 4 5 6 7 |
$.get( "test.php" ).then( function() { alert( "$.get succeeded" ); }, function() { alert( "$.get failed!" ); } ); |
うわーわかりやすい・・・。
結局、中身が分からない人にDefferedオブジェクトみたいな状態をもつ言語パーツを使いこなすのは無理なのです。ここから状態を排除し、get.thenになってこそDSLと言えると思っています。
Deferredをバグらせずに書くことは一握りのプログラマの特権でした。get.thenを書けない人はほぼいません。
時間が解決したのです。
Scalaは良いアイディア、DSLの書きやすさ、結果としてのコードの短さという、時間に頼れる要素をたっぷりと持っています。
それがポストJavaの主流という形かはわかりませんが、絶対何らかの形で生き残ると思います。
てゆうかアメリカ人・中国人・インド人にScala書ける人たくさんいて求人市場もあるので、すでに生き残ってます。
なので、Scalaは学ぶ価値があるのではないでしょうか。
ピンバック: KMeans法のソースコードを公開します | teqニカルブログ
ピンバック: pythonの歴史(〜2016年) : from Data Science Perspective | The Big Computing