別の言語
Crystalにはグローバルな型推論があります。いくつかのケース(ジェネリック型引数)を除いて、型注釈なしでプログラミングできます。
これは諸刃の剣です。
一方では、型について明示する必要がないのは本当に素晴らしいことです。これにより、動的言語と同様に、非常に迅速なプロトタイピングが可能になります。アイデアをすばやくスケッチし、常に再入力することなく進化させることができます。これは、コードのリファクタリングや再編成を行う際に、摩擦が非常に少ないためにも役立ちます。たとえば、コードの一部からメソッドを抽出するときは、引数の名前を指定するだけで、コンパイラーが引数の型と戻り値の型を判断します。また、保持する可能性のある型を最初に宣言しなくても、何らかの値を代入してインスタンス変数をすぐに使用できます。
しかし、このアプローチにはいくつかの欠点もあります。それらを分析してみましょう。
コードが理解しにくく、追跡しにくくなる
型注釈がないと、コードが追跡しにくくなると言う人もいます。例を見てみましょう。
def sum(values)
count = 0
values.each do |value|
count += value
end
count
end
values
の型は何ですか? どのような型で動作しているかを知らずに、このコードを理解することはどうすればできますか?
あなたがプログラミングを学んだとき、ある時点で疑似コードについて紹介されたと確信しています。これは実生活で見られるコードと似ていますが、簡略化されています。そこでは型注釈はほとんど見られません。型は与えられた文脈では明白なものです。型注釈やその他の情報を追加すると、コードの意図が見えにくくなります。聞き覚えがありますか?
私たちの見解では、Rubyコードは疑似コードに非常に近いものです。上記のコードには、型注釈はありません。アルゴリズムは明確です。values
内の各項目を反復処理し、count
変数に追加します。それだけです。values
の型は何ですか?それは実際には重要ではありません。私たちが気にするのは、それが反復処理できること(each
で)と、合計できるということです。さらに、sum
、values
、count
という名前は、メソッドの意図と変数の考えられる型を理解するのに役立ちます。
これを、いくつかの型を追加する必要がある他の言語と比較してください。
interface Iterable<T>
def each(&block : T ->)
end
interface Addable<T>
def +(other : T)
end
def sum(values : Iterable<T>) where T : Addable<T>
count = 0
values.each do |value|
count += value
end
count
end
ここでは、コンパイラーに、ジェネリック型T
の要素を生成するeach
メソッドを持つ型Iterable
があることを伝えています。次に、コンパイラーに、同じ型の値で操作するメソッド+
を持つ型Addable<T>
があることを伝えます。最後に、T
がAddable<T>
を実装する、Iterable<T>
型に属する値で操作するsum
メソッドを定義します。
私たちにとって、この最後のコードは、私たちが念頭に置いていた疑似コードから遠く離れています。また、読むべきコードも、理解すべきコードも増えています。
これに対する反論として考えられるのは、コードのナビゲーションが簡単になったということです。each
がどのように実装されているか、またはAddable
が何であるかを知りたい場合、どこでそれを見つけるべきかを知っています。型注釈がない場合、動的言語ではそれを行うことはできません。
しかし...ちょっと待ってください! Crystalは動的言語ではありません。コンパイルが終了すると、使用されたすべての変数とメソッドに型が割り当てられます。その情報から、コードを再作成して閲覧できるようにできます。そして実際、Crystalには(非常に実験的な)ツールがあり、crystal browser file.cr
でコードをコンパイルすると、それを使用できます。これにより、Webブラウザーが開き、変数の型を表示したり、メソッドをナビゲートしたりできます。したがって、私たちの意見では、これはここでの正当な理由ではありません。
この最後のことは、Crystalがクラスのインスタンス変数の型を知っていることも意味します。Rubyでは、@foo
インスタンス変数を持つクラスを見て、その型が何なのか疑問に思うかもしれません。心配しないでください。crystal hierarchy file.cr
を実行すれば、正確な型がわかります。これは、クラスの使用状況を示すため、スペックファイルで実行すると最も役立ちます。そして、将来的には、この情報をRDocのようなドキュメント形式で表示できるようになります。
インクリメンタルコンパイルは不可能
型注釈がないため、コンパイラーは毎回、すべてを最初から型を把握する必要があります。モジュールをオブジェクトファイルやその他の形式にコンパイルし、後でその情報を再利用する方法はありません。なぜなら、一見して(型)情報がないからです。
幸いなことに、Crystalのコンパイラーは非常に高速です。コンパイラー全体をコンパイルするのに5〜10秒かかります(そのうち2.3秒は型推論フェーズで費やされています)。大規模なプログラムでは時間が長くなるため、この問題の解決策を見つける必要があります。
別の問題は、REPLを実行するのが難しいことです。コードに常に型注釈がある場合、マシンコードを生成でき、変数の型が変更されたり、型のインスタンス変数が作成または変更されたりする可能性を心配する必要はありません。
何度も私たちは諦めそうになりました。「プログラマーがインスタンス変数とメソッドに型注釈を追加することを要求すれば、インクリメンタルコンパイルとREPLが現実になるかもしれない」、「少なくとも構文は快適だろう」と。幸いなことに、私たちがそう言うたびに、別の人が「NO」と大きく答えました。
この「NO」には非常に強い理由があります。その方向に言語を変更すると、別の言語になってしまいます。Crystalは、型注釈を省略できる言語です(本当にそうしたい場合は、型注釈を追加できます)。しかし、型注釈を強制的に追加すると、それはもはやCrystalではありません。既存のプログラミング言語の1つと非常に似ているでしょう。そして、なぜ私たちはそうしたいのでしょうか? 既存の(または開発中の)別の言語と似た別の言語を発明するメリットは何でしょうか? ありません。それは時間の無駄であり、努力の重複になるでしょう。
確かに、諦めなければ、より困難な課題に直面することになるでしょう。インクリメンタルコンパイルは本当に不可能なのでしょうか? 同じような技術を考えることはできないでしょうか? この言語でREPLを何とか実行することは可能でしょうか? 非常に興味深い疑問が生じます。困難な問題が発生します。楽しい時間が訪れます。
型注釈がコンパイラー側から必須ではないが、型は依然として存在するという言語が可能だと信じています。私たちはスマートなコンパイラーを望んでいます。コンパイラーの作成者である私たちの仕事を簡単にするために、言語を単純化したいとは思っていません。プログラマーの仕事をより簡単にするために、そうしたいのです。そして、楽しい :-)