Crystal

本書は、Crystalのチュートリアルです。 Crystalは、Ary Borenszweig、Juan Wajnerman、Brian Cardiffと300人以上の貢献者によって設計・開発された汎用オブジェクト指向プログラミング言語です。Ruby にヒントを得た構文を持ち、静的型付けなコンパイル型言語ですが、変数やメソッドの引数の型は一般には不要です。型は高度なグローバル型推論アルゴリズムによって解決されます。CrystalはApache Licenseバージョン2.0のもと、FOSSとしてリリースされています。
Crystalは、Rubyのシンタックスを持つ静的型付けのプログラミング言語で、以下のような特徴があります。
お約束のHello_worldですが、ここではRubyとの比較も兼ねて、Ruby#Hello, World!をそのまま実行してみます。
hello.crを用意します。
実際にCrystalで書かれたのコードを見てみましょう。
Crystalを使ってファイルを読み込むには、Fileクラスを使用します。
以下は、Crystalでファイルを読み込む例です。
File.openにブロックを渡すと、ブロックを抜けると自動的にファイルが閉じられます。明示的なファイルのクローズが不要です。
このコードは、ファイルを開いて内容を読み込む際にブロックを使用し、ブロックを抜けるとファイルが自動的に閉じられるというCrystalの一般的なイディオムを示しています。 この方法は、ファイルの操作後にクローズ処理を忘れることを防ぎ、コードをより安全かつ簡潔にします。
エラトステネスの篩を、若干 Crystal らしく書いてみました。
Crystalのプログラムを作り、コンパイル・実装するには、「オンライン実行環境を使う」・「エディト・コンパイル・実行環境を用意してそれを使う」の2通りの方法があります。
公式のオンライン実行環境、 https://play.crystal-lang.org/ があります。 まずは、これを使って本書に例示されているコードを実行してみることをお勧めします。
エディタについては本書では触れませんが、プログラミング時間の大半はエディタの操作に費やされるため、良いエディタを選択することが重要です。
Crystal の言語処理系は、 https://crystal-lang.org/install/ から入手します。
自分の、OSやGNU/Linuxであればディストリビューションに合わせてインストールしてください。 また、FreeBSDのように crystal と shards が別パッケージとなっていることもあるので、その場合は shards も追加インストールします。
多くの場合、インストールされた crystal はスタティック リンクされているので、ダイナミック リンク版の crystal を入手するには、ソースコードからビルドします。 また、interactive Crystalを有効にするためにも、ソースコードからのビルドが必要です。
crystal は、ソースコードが Github の https://github.com/crystal-lang/crystal.git で公開されているので、必要に応じてソースコードからビルドすることができます。 crystalは、セルフホスティング言語なので、最初にバイナリーを入手してブートストラップするか、クロスビルドしたバイナリーを持込むか、パッケージシステムからインストールし、ターゲットでセルフコンパイル出来る状態を作る方法があります。
ビルドには、Chromebook(メモリー4GB, Celeron N4000, OS Version: reven-release/R104-14909.124.0, Chromebrew version: `1.25.3`, llvm-15.0.0)で約30分かかりました。
crystal コマンドは Crystal のコンパイラであると同時に、ビルドツールなどを含んだツールチェインです(プログラミング言語のCrystalは、先頭を大文字、コマンドのcrystalは、先頭を小文字にして区別します)。
[TODO: コマンドラインツール crystal の解説。 crystal ファイル名 は crystal run ファイル名 の短縮形で、インタープリタ的な実行...ではなく、内部ビルドツールでコンパイル・実行を行う]
Crystalは、Rubyに触発された構文を持つものの、Rubyとの互換性をゴールに定めてはいません。 このため、細部を見ると仕様に差異があり、Rubyのソースコードをcrystalに掛けても前節の 'Hello World' の様にコンパイルに失敗することがあります。 また、コンパイルできても実行結果に違いが出ることがあります。
ここでは、Ruby との違いについて実際のコードと双方の結果を比較することで、差異についての理解を深めていきます。
big を require すると BigInt が使えるようになります。
Crystalでは、Set(集合)は組込みクラスなので、require "set"は不要です。
Crystal には、Ruby にはある for式がありません。
また loop 式もありませんが while true; ... end で間に合います。Ruby では while 式の条件の次に do が置けますが、Crystal では置けません。
Rubyのforに似せるという縛りがなければ、(マクロを使うまでもなく)簡単に実装できます。 偶然ですが、Scalaのforメソッドに似てしまいました(あれも、イテレーション メソッドに展開されるのである程度は必然)。Scalaと同じ様にジェネレターと組合わせて多次元に拡張することもできそうです。
Crystal には eval() はありません。 Crystalはコンパイル型言語ですので、無理もないことです。 もし、Crystal で eval() を実装しようとすると、Common Lisp の様にインタープリターを丸ごとランタイムに含む必要があります。 これはリーズナブルな選択ではありません。 Crystal では、eval() が必要なケースに(限定的ですが)マクロを使うことで実現出来る可能性があります。
Rubyでは、Arrayなどのeachメソッドは self を返し、break で中断すると nil を返します。
そこで、このようなコードがかけます。
これに対し Crystal では、Arrayなどのeachメソッドは self を返さない(常に nil を返す)ので上記のようなトリッキーなコードはかけずフラッグを使います。
Crystalには、Rubyにはないマクロがあります。Rubyは実行時にすべてのオブジェクトにアクセス出来て、メソッド生やし放題なのでマクロは必要ありませんが、Crystalはコンパイル時に型やメソッドを確定する必要があり、特にメソッドジェネレターとしてのマクロにニーズがあります。また、テンプレート言語的なマクロなので、環境変数による条件分岐や、コンテナを渡し繰返し処理する構文もあります。


Ruby#ユーザー定義クラスの都市間の大圏距離を求めるメソッドを追加した例を、Crystalに移植しました。
JavaScript/クラス#包含と継承を、Rubyに移植したRuby#包含と継承を、Crystalに移植しました。
Crystal には、RubyのClassにあるメソッド superclass と subclasses がないので、マクロで実装しました。
Java/抽象クラスを、Crystalに移植しました。
RubyとCrystalの主な文法の違いを表形式でまとめました。
Crystalのキーワード( keywords ) は、以下の通り。
abstract alias as asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof lib macro module next of out pointerof private protected rescue return require select sizeof struct super then type typeof uninitialized union unless until when while with yield
Crystalは、1つ、2つ、または3つのオペランドを持つ数多くの演算子をサポートしています。
演算子式は、実際にはメソッド呼び出しとしてパースされます。例えば、a + b は a.+(b) と意味的に同じで、引数 b を持つ a のメソッド + を呼び出すことになります。
制御構造(せいぎょこうぞう、control flow)とは、「順次」「分岐」「反復」という基本的な処理のことを言います。
Crystalの条件分岐には、if, until と caseの3つの構文があります。
ifは条件式によって実行・否を切り替える構造構文で、評価した式の値を返すので条件演算子でもあります。
Crystalには、RubyやPerlのような後置のifがあります。
unless(アンレス)は条件式によって実行・否を切り替える構造構文ですが、ifとは条件式に対する挙動が逆です。
Crystalには、RubyやPerlのような後置のunlessがあります。
caseは、複数の条件式によって処理を降る分ける用途の為に用意されています。
ifを使ったコードをcaseに書き換えてみましょう。
このコードは when 節の式の値とcaseの式を === で比較し、最初に一致した when に対応する式が実行される事を利用しています。
when 節が式ではなく型であった場合、caseの式を is_a? で評価し、最初に一致した when に対応する式が実行されます。
暗黙のオブジェクト構文を使うと
when に対応する式は、1つのことが珍しくないので、その場合は省略可能な then を補うと、1行で書けます。
Crystalにおいて、タプルとは、複数の値をグループ化して1つのオブジェクトとして扱うための機能です。タプルは、角括弧 [ ] 内にコンマで区切られた値を指定して作成します。
例えば、次のように2つの値を持つタプルを作成できます。
タプルの要素にアクセスするには、インデックスを指定して取得します。
また、Crystalにはダミー識別子 _ があります。この識別子は、未使用の引数や変数を表すために使用されます。例えば、次のように _ を使って、2番目の要素を無視して1番目の要素だけを取得することができます。
ここでは、2番目の要素を _ で受け取っています。このように、ダミー識別子 _ を使うことで、ある変数や引数が使用されないことを明示することができます。
when の代わりに in を使用すると、exhaustive case 式が作成されます。exhaustive case では、必要な in 条件を省略するとコンパイル時にエラーとなります。exhaustive case 式では、when 節と else 節を含むことはできません。
[TODO:短絡評価 && || ]
Crystalには、他のプログラミング言語のような繰返し構文と、イテレーターメソッドがあります。
Crystalの繰返し構文には、while と untilの2つがあります。
while(ホワイル)は条件が真である間、式を実行しつづけます。
until(アンティル)は条件が偽である間、式を実行しつづけます。whileとは条件に対する挙動が逆です。
Crystalにはforがありませんが、コレクションのイテレーションメソッドを使うことで繰返しを簡素に実現出来ます。
Rangeオブジェクトは、整数の区間を表し範囲演算子 開始 .. 終了 や 開始 ... 終了 で生成します。 範囲演算子の終了は省略でき、その場合は数学の半開区間(半閉区間)となり、例えば、1 ..は自然数となります(ただし、日本的な0を自然数に含まない場合)。
Arrayオブジェクトは、任意の Crystal オブジェクトを要素として持つことができます。 配列式[ 要素1, 要素2, ... 要素n ] で生成します。
Tupleオブジェクトは、任意の Crystal オブジェクトを要素として持つことができます。 配列式{ 要素1, 要素2, ... 要素n } で生成します。
Setオブジェクトは集合です。任意の Crystal オブジェクトを要素として持つことができますが、1つの値は重複して持てません。 Set.newに配列式{ 要素1, 要素2, ... 要素n } などを渡し初期化します。
NamedTupleオブジェクトは、任意の Crystal オブジェクトをキーに、任意の Crystal オブジェクトを値に持つことができる連想配列です。 NamedTuple式{キー1 => 値1, キー2 => 値2, キーn => 値n} で生成します。 また、キーが Symbol の場合 NamedTuple式{キー1: 値1, キー2: 値2, キーn: 値n} で生成することが出来ます。
このように、Crystalではforがなくてもコレクションのメソッドで同様の処理を実現できます。
loop ありません。 while true で代用します。
Integer#timesは与えられたブロックをオブジェクトの示す整数値回くりかえします。
Crystal では、全てがオブジェクトです。
オブジェクトの値や機能を呼び出すためには、メソッドを使います(多くの演算子もメソッドです)。
Crystal には、Objectクラスにmethodsメソッドがないので、マクロで実装しました。

マクロには {{attr.id}} の様にASTへのアクセス手順が用意されており、半ば言語を拡張するようなアプローチを取ることも出来ます。
Crystalのマクロは、実行時ではなくコンパイル時に実行される機能であり、ソースコードを操作することができます。以下はCrystalのマクロの主な機能と構文の説明です。
マクロを定義するときに、引数に * を付けることができます。これにより、任意の数の引数を受け取ることができます。例えば、以下のマクロは、任意の数の引数をコンマで区切った文字列を出力します。
このマクロを使用すると、以下のようにコードを記述できます。
マクロの中で、{{}}で囲んだ式を埋め込むことができます。この式は、コンパイル時に評価され、その結果がコードに挿入されます。例えば、以下のマクロは、引数に指定された式を2倍にして返します。
このマクロを使用すると、以下のようにコードを記述できます。
マクロの中で、{% ... %}で囲んだ部分は、コンパイル時に評価されますが、その結果はコードに挿入されません。代わりに、{% ... %}で囲まれたコードは、コンパイル時に実行されます。例えば、以下のマクロは、コンパイル時に現在の日時を表示します。
このマクロを使用すると、以下のようにコードを記述できます。
以上がCrystalのマクロの主な機能と構文の説明です。マクロを利用することで、より柔軟で効率的なコードを記述することができます。
Crystalのマクロは、条件分岐や反復処理を行うことができます。以下に、それぞれの構文と使用例を示します。
条件分岐は、if/elsif/elseやcase/when/elseのような構文を使用することができます。マクロ内では、条件分岐によって生成するコードを制御することができます。
反復処理は、whileやuntil、forのような構文を使用することができます。マクロ内では、反復処理によって生成するコードを制御することができます。
メソッド p は、与えられた「式」の inspaect() の返す値を puts しますが、マクロ p! は、それに先んじて(評価前の)「式」を表示します。
マクロ p! は入れ子に出来ます。また、一旦ASTに変換してから再度ソースコードに変換するので、等価な別の構文に変換されることがあります。