コンテンツへスキップ
GitHubリポジトリ フォーラム RSS-ニュースフィード

Crystalの未来

Ary Borenzweig

(この記事はCrystalアドベントカレンダー2015の一部です)

クリスマスイブのこと、奇妙な出来事がありました。私たちはCrystalで楽しくコーディングしていたのですが、目を離した瞬間、半透明な人物が近くに現れたのです。その存在は近づいてきて言いました。「私は過去のクリスマスの幽霊だ。私と一緒に来なさい。」

私たちは、Rubyに似ているがコンパイルされ、型安全な新しい言語をコーディングしている自分たちを見ていました。その時点では、言語は本当にRubyのようでした。空の配列を作成するには[]と書き、空の集合を作成するにはSet.newと書くのです。そして私たちは幸せでした。コンパイル時間が膨大で、指数関数的に増え、耐え難いものだと気付くまでは。

私たちはそれを機能させるためにひどい時間を費やしましたが、無駄でした。最終的に、変更することにしました。空のジェネリック型の型を指定するのです。例えば[] of Int32Set(Int32).newのように。コンパイル時間は正常に戻りました。そして私たちは再びある程度幸せになりましたが、同時にRubyの感触の一部を失っていると感じました。言語は分岐しました。

私たちは過去のクリスマスの幽霊に、それが何を意味するのか尋ねようとしましたが、その場所には似て非なる存在がいました。彼女は言いました。「私は現在のクリスマスの幽霊だ。私と一緒に来なさい。」

私たちの周りには、活気のある小さなコミュニティがCrystalでプログラミングしていました。彼らは幸せそうでした。ジェネリック型の型を指定しなければならないという煩わしさは全くありませんでした。誰もが、Rubyの精神が何らかの形でまだ存在していると感じていました。使い慣れたAPIやクラス、構文、強力なブロックの中に。さらに、CPUと並行処理の両方におけるパフォーマンスの向上と、より優れた型安全性によって、本当に効果があったため、ときどき型を指定しなければならないことさえ煩わしくありませんでした。

再び、私たちは幽霊に意味を尋ねようとしましたが、その場所にはまた別のもの、機械的な結晶質の存在がいました。それは言いました。「私は未来のクリスマスの幽霊だ。私について来なさい。」

小さなコミュニティはまだCrystalでプログラミングしていましたが、ほとんどの人が以前ほど幸せそうではありませんでした。私たちは彼らに理由を尋ねようとしましたが、誰も私たちの存在に気づきませんでした。私たちはコンピュータを使ってインターネットでCrystalを検索しようとしましたが、私たちの腕は何にも触れることができませんでした。私たちは好奇心旺盛な顔で幽霊に目を向けると、その胸にキーボードと小さな画面が付いていることに気づきました。私たちは「Crystal sucks」を検索しました。そうすれば、この言語に関する苦情の投稿が表示されるはずです。そして確かに、いくつかありました。ほとんどは、膨大なコンパイル時間とメモリ使用量に関するものでした。「膨大なコンパイル時間?」と私たちは考えました。「私たちは数年前にもう解決したはずだ!」と私たちは幽霊に叫びました。私たちが得た唯一の返答は「コンパイル中…」であり、ビジョンは消え、私たちは一人になってオフィスに戻っていました。

現在に戻る

「少し計算してみよう」と言いました。現在Crystalで持っている最大のプログラムはコンパイラで、約4万行のコードです。コンパイルには約10秒かかり、そのためには940MBのメモリが必要です。私たちのRailsアプリの1つは、gemの総行数と「app」ディレクトリのコードを合計すると、約32万行のコードで、コンパイラの8倍です。それをCrystalで書き直したり、少なくとも同様の機能を持つアプリケーションを作成したりすると、毎回80秒のコンパイル時間と8GBのメモリが必要になります。変更のたびにこれだけの待ち時間とメモリは、非常に大きすぎます。

現在の言語でこの状況を改善できますか?インクリメンタルコンパイルを導入できますか?以前のコンパイルのセマンティック結果(推論された型)をキャッシュし、次のコンパイルに使用する方法は何かを検討しました。メソッドの型は、引数の型、呼び出すメソッドの型、インスタンス変数、クラス変数、グローバル変数の型にのみ依存するという観察があります。

そこで、プログラム内のすべての型の推論されたインスタンス変数の型と、メソッドインスタンス化とその依存関係(そのメソッドが依存する型、具体的には呼び出す他のメソッド)をキャッシュするというアイデアがあります。インスタンス変数の型が同じままであり、メソッドのコードが変更されておらず、依存関係(呼び出されたメソッド)が変更されていない場合、前のコンパイルの結果(型と生成されたコード)を安全に再利用できます。

上記の「if」は「インスタンス変数の型が同じままである場合」で始まります。しかし、それをどのように知ることができるのでしょうか?問題は、コンパイラがプログラムをトラバースし、メソッドをインスタンス化し、何が代入されているかをチェックすることで、それらの型を決定することです。そのため、プログラム全体を型付けするまで最終的な型を知ることはできないため、キャッシュを実際に再利用することはできません!これは鶏と卵の問題です。

解決策は、インスタンス変数、クラス変数、グローバル変数の型を指定することのようです。これにより、メソッドの型付けが完了すると、その型は変更されなくなります(インスタンス変数など、メソッドに対して非ローカルなものは、それ以上変更できなくなるため)。その情報をキャッシュして、次のコンパイルに再利用することができます。それだけでなく、キャッシュがなくても、型推論ははるかにシンプルで高速になります。

これが正しいことでしょうか?再びRubyから少し逸脱することになります。どのような未来を望んでいますか?コンパイルごとに多くの時間を費やすという代償を払って、現在の方法を維持すべきでしょうか?それとも、さらにいくつかの型を指定して、より機敏な開発サイクルを持つ方が良いのでしょうか?

私たちが本当に望んでいるのは、使いやすく効率的な言語です。コンパイルが完了するのを長時間待つのは全く楽しくありません。ときどきいくつかの型に注釈を付けるよりも、はるかに楽しくありません。そしてこれらの型は、ジェネリック、インスタンス、クラス、グローバル変数のためだけのものであり、ローカル変数やメソッド引数には型注釈は必要ありません。新しいメソッドを記述したり、プログラムをコンパイルしたりする回数と比較して、これらの型が変更される頻度は非常に少ないことを考えると、変更する価値のあるものだと思います。

多くの既存のコードが壊れるため、できるだけ早くこれを実現したいと考えているため、この新しいコンパイラの開発を開始しました。現在のコンパイラはASTを直接操作しますが、新しいコンパイラではフローグラフを使用します。これにより、よりシンプルなコンパイラ(誰でも理解してすぐに参加して貢献できるコンパイラ)と、理解しやすく最適化しやすいコードを持つことができます。また、フローグラフはサイクルや「goto」のようなジャンプを許可するため、Rubyのretryのような新しい機能を最小限の労力で導入することも可能になります。

この変更の詳細を知りたい場合は、トラッキングチケットがあります。

質問と回答

  • 新しいコンパイラはいつ完成しますか?まだわかりません。私たちはゆっくりと着実に作業を進めており、可読性、拡張性、効率性を念頭に置いて書き、まず最も難しい部分に焦点を当てています。言語がサポートするほとんどの機能がわかった今、作業が容易になっています。現在のコンパイラは実験として、そしてRubyで書かれたコンパイラの移植として始まったことを覚えておいてください。そのため、そのコードは最高のCrystalコードではありません。
  • 現在のコンパイラで作業を続けますか?はい、いいえ。バグが簡単に修正できる場合は修正し、標準ライブラリの拡張と改善を続けます。
  • 私のコードはすべてコンパイルできなくなりますか?おそらく。ただし、現在のコンパイラのtool hierarchyを使用してインスタンス変数の型を問い合わせることで、アップグレードを容易にすることができます。実際、自動でアップグレードを行うツールを含める可能性があります。それほど簡単なのです。
  • 新しいコンパイラには他の機能も含まれますか? そう願っています!この変更により、通常の&block構文でフォワーディングブロックをサポートする予定です。現在でもこれは可能ですが、常にクロージャを作成することになり、より良い方法があります。また、ブロックによる再帰呼び出しも許可する予定です。これはRubyでは可能ですが、Crystalでは不可能です。さらに、Array(Object)またはArray(T)を任意の種類のTで使用できるようにしたいと考えています。これも、現在の言語バージョンでは完全に可能ではありません。したがって、これらの新しい型注釈は、補償として言語にさらに多くの機能をもたらします。
  • 将来的にもこのような破壊的な変更はありますか? 答えは「いいえ」と確信しています。インスタンス変数、クラス変数、グローバル変数の型がわかっていれば、メソッドが与えられた場合、selfの型とその引数の型を、そのメソッドとそれが呼び出すメソッドを分析するだけで推論できます。現在、これは不可能です。なぜなら、一部のメソッドの型は、クラスの使用方法(クラスに割り当てるもの)によって異なるからです。したがって、この変更が最後の大きな破壊的変更となります。