FIVETEESIXONE

(翻訳) The Future of Crystal


Crystal Advent Calendar 2015 に、なんと Crystal の作者である Ary Borenszweig (@asterite) 本人が記事を投稿してくれました。

せっかく日本のコミュニティの Advent Calendar に向けて書いてくれたし、内容も Crystal にとって非常に重要なものなので、これも日本語に翻訳したものを公開したいと思います (クリスマス・キャロルを下敷きにした構成も洒落てていいよね!) 。
原文はこちら。The future of Crystal


The Future of Crystal

(この記事は Crystal Advent Calendar 2015 の 24 日目の記事です)

クリスマスイブに不思義な体験をした。

僕たちは Crystal のコーディングを楽しんでいたんだけど、ちょっと画面から目を離した瞬間、すぐそばに透き通った人影が現れたんだ。そしてそれは近づいてきてこう言った。

「私は過去のクリスマスの精霊。さあ、一緒に行きましょう。」

そこには、Ruby によく似た、だけどコンパイルされて型安全な新しい言語を使ってコーディングしている僕たち自身の姿があった。そのとき、その言語は本当に Ruby によく似ていたよ。空の Array を作りたければ [] と書けるし、空の Set は Set.new で作ることができた。僕たちは幸せだったね。でもそれも、途方もなく長くて、指数関数的に増加して、到底我慢できないコンパイル時間に気がつくまでの話だった。僕たちは悲しみに暮れてしまった。

それを何とかしようとして必死にがんばってみたんだけど、結局その努力はすべて徒労に終わった。そして、とうとうこんな風に変える決断をしたんだ。例えば、[] of Int32 だとか Set(Int32).new みたいな感じで、空の Generic 型には型を指定しなくちゃいけないように。こうして、コンパイル時間は正常に戻った。僕らはまたハッピーになったよ。でも同時に、Ruby のフィーリングをどこかに置いてきてしまったような気がしていた。このとき、その言語は Ruby とは違う道を歩みはじめた。

僕たちは、過去のクリスマスの精霊の方に振り向いて聞いた。「いったい僕たちに何を見せたいんだい?」でも彼は、それまでと似ているけれど少し違った姿に変身してこう言った。

「私は現在のクリスマスの精霊よ。さあ、こっちへいらっしゃい。」

すると、僕たちの周りに、小さいけれど活気に満ちたコミュニティが現れ、みんな Crystal でプログラムを書いていた。彼らは幸せそうに見えた。Generic 型に型を指定することへの不満なんてどこにもないようだったよ。みんな、まだそこに、慣れ親しんだ API やクラスの中に、シンタックスの中に、パワフルなブロックの中に、Ruby の精神がちゃんと生き続けていることを感じていた。しかもそれだけじゃなくて、型の安全性はより優れたものになっていたし、CPU と並行性の両方の面でパフォーマンスも素晴しく向上していた。本当に大成功だったね。だから、時々型を指定してやる必要があるくらい、大して面倒なことでもないと思ったよ。

僕たちはもう一度、精霊の方を見て尋ねた。「どうやら僕たちの判断は正しかったみたいだね?」でも、またさっきと同じように、その姿は別の何かに変わっていた。その機械仕掛けの輝く人影はこういった。

「私は未来のクリスマスの精霊。さあ、ついてきなさい。」

さっきの小さなコミュニティはまだ Crystal でプログラミングしているみたいだった。だけど、彼らの姿は以前のように幸せそうには見えなかった。どうしてなのか尋ねようとしたけれど、彼らには僕たちの存在が認識できないようだった。だから次はコンピューターを使って、インターネットで Crystal のことを調べようとした。でも僕たちの手は物に触れることもできなかった。そのとき、精霊の胸のところにキーボードと小さい画面があることに気がついたんだ。だから僕たちはその端末を使って、「Crystal sucks」という言葉で検索した。きっとその結果で、言語のどこが気に入らないのかがわかると思ったから。実際、たくさんの不平不満が見つかったよ。そして、そのほとんどは「長すぎるコンパイル時間」と「多すぎるメモリ使用量」についてだった。それを見た僕たちは精霊に向かって大声で叫んだ。「コンパイル時間が長いだって?もうそれは僕たちが何年も前に解決したことじゃないか!」だけどその返事は一言「コンパイル中です…」というものだけだった。そして、ふいにその姿は消えてしまい、残された僕たちだけがオフィスに佇んでいた…

現在に戻って

「ちょっと計算してみよう。」

今、僕たちにとって一番大きな Crystal のプログラムはコンパイラで、そのコードは 4 万行ほどある。それをコンパイルするには 10 秒かかって、940MB のメモリを消費する。じゃあここで、僕たちが開発した Rails アプリケーションの 1 つを見てみよう。そのアプリケーションで使っている Gem と「app」ディレクトリのコードを合計すると 32 万行だった。つまり、さっきのコンパイラの 8 倍あるってことだ。もしこのアプリケーションを Crystal で書き直したとしたら…もしくは、同じような機能を持ったアプリケーションを Crystal で開発したとしたら…きっとそれをコンパイルするのには 80 秒もかかって、そのたびに 8GB ものメモリを消費することになるだろう。ということは、ちょっと変更しただけで、毎回そんなに長い時間待たされて、ひどいメモリの消費量に悩まされるってことだ。

現在の言語そのままでこの状況を改善することはできるだろうか?差分コンパイルは実現可能なのか?僕たちはまず、以前のコンパイルでの解析の結果 (推論された型) をキャッシュし、以降のコンパイルでそれを利用する方法を考えた。そして、メソッドの型というものが、引数の型と、そのメソッドが呼び出すメソッドの型と、インスタンス変数、クラス変数、そしてグローバル変数の型に依存しているってことがわかった。

そこで、1つのアイデアとして、プログラム内に使われるすべての型について、推論されたインスタンス変数の型をキャッシュし、同時にそれらが依存している対象 (そのメソッドが依存している型と、そのメソッドが呼び出すメソッドの型) をキャッシュする、という案を考えた。もしインスタンス変数の型が変わっていなければ、メソッドのコードは変更されていないし、依存している対象 (呼び出されるメソッド) も変更されていないことがわかる。したがって、(型や生成されたコードといった) 以前のコンパイルの結果を安心して再利用できるというわけだ。

ただ、「もしインスタンス変数の型が変わっていなければ」という「もし」の条件を、いったいどのようにして知ることができるのだろう?問題は、コンパイラがそれらの型を決定するのが、プログラム全体を読んで、メソッドを初期化して、そしてそれらに何が代入されるのかをチェックして…そうしてはじめて可能になるものだということだ。つまり、結局プログラムのすべての型が決まるまでは、その最終的な型を知ることができず、キャッシュを再利用することも不可能ということじゃないか!これはまさに「卵が先か?鶏が先か?」の問題だった。

この問題を解決するには、インスタンスやクラス、そしてグローバル変数の型を指定するしかないように思える。そうすれば、(インスタンス変数のようにメソッドローカルではないものがすべて不変となるため) メソッドの型はずっと変わることがなくなり、その情報をキャッシュすることで以降のコンパイル時に再利用することができる。また、それだけに留まらず、キャッシュを使わなかったとしても、型推論は今よりずっとシンプルで高速なものになるだろう。

だけど、本当にこれは正しいことなのか?これでまた一段と Ruby から遠いものになってしまうんじゃないのか?僕たちはいったいどんな未来を望んでいるんだろう?コンパイルで毎回長いこと待たされるのを我慢してでも、今のアプローチのまま続けるべきだろうか?それとも、より多くの型を指定しなくちゃいけないけれど、もっと高速な開発サイクルを実現できる方が望ましいのだろうか?

私たちが心から望んでいるもの、それは「使うのが楽しくて」そして「生産性の高い」言語だ。コンパイルが終わるまで長い時間待つだって?そんなの全然楽しくないね。ときどき型を指定しなくちゃいけないとしても、長いこと待たされるよりはマシだ。しかも、指定しなくちゃいけない型は Generic 型の変数とインスタンス変数、そしてクラス変数とグローバル変数だけで、ローカル変数やメソッド引数には型を指定する必要ないんだから。そういった型が変更される頻度に比べて、新しいメソッドを書いたり、プログラムをコンパイルする頻度がどれほど多いか考えてみるといい。きっとこの変更には大きな価値があるに違いない。

僕たちはすでに、その新しいコンパイラの開発をスタートしている。なぜなら、この変更によって多くの既存のコードが動かなくなるので、できる限り早く完成させたいと思っているから。そして、現在のコンパイラが直接 AST を扱っているのに対して、開発中の新しいコンパイラはフローグラフを扱うものになる。これによって、コンパイラは (誰にとってもわかりやすくてコントリビュートしやすいような) シンプルなものになるので、理解しやすくなるし、コードの最適化もやりやすくなる。おまけに、フローグラフではサイクリックな処理や「goto」のようなジャンプが可能なので、Ruby の retry のような機能を容易に提供することもできるようになる。

もし、この変更について詳しく知りたければ、この issue を追ってもらえれば、と思う。

Q & A

新しいコンパイラはいつ完成するの?

まだはっきりと言えません。ただ、ゆっくりとですが着実に開発を進めており、可読性や拡張性、そして生産性に注意を払いながら、最も難しい部分から実装を進めています。また、以前とは違って今は言語がサポートすべき機能を明確に把握できていることが大きいので、実装することは比較的容易です。現在のコンパイラは元々実験的にスタートしたもので、Ruby で書かれたコンパイラのポーティングだったため、理想的な Crystal のコードとは言えません。

現在のコンパイラの開発は継続するの?

「Yes」でもあり「No」でもあります。簡単に fix できるバグであれば修正するでしょう。また、標準ライブラリの拡充や改善は継続します。

今のコードはコンパイルできなくなるの?

おそらくそうなります。ただ、現在のコンパイラの tool hierarchy を利用することでインスタンス変数の型を知ることができるので、アップグレードはそう難しくないでしょう。実際、自動的にアップグレードするためのツールを提供することも検討しています。それほどシンプルな対応でアップグレードが可能になるはずです。

新しいコンパイラには他にも新機能が搭載されるの?

そうしたいと思っています!この変更によって、ブロックのフォワーディングを通常の &block というシンタックスで書けるようにするつもりです。今でもこれは可能ではありますが、必ずクロージャを生成してしまうため、もっと改善できると考えています。また、Ruby ではできるのに Crystal ではできないことの 1 つである、再帰的なブロック呼び出しも検討しています。更に、現在のバージョンでは不可能な、T が何でもとれるような Array(Object) もしくは Array(T) というものも実現したいと思っています。このように、新しい型注釈によって言語の機能はより強力なものになるでしょう。

将来、こういった破壊的な変更がまたあるの?

いいえ、無いと言い切れます。もし、インスタンス変数とクラス変数とグローバル変数、そして与えられたメソッドを知ることができて、self の型と引数の型がわかれば、そのメソッドとそのメソッドで呼び出されるメソッドを分析することで型を推論することができるようになります。現在は、メソッドの型がクラスの使い方 (何を代入するか) に依存することがあるためにこれを知ることが不可能です。したがって、この変更が最後の大きな破壊的変更となるでしょう。