[crystal] Ruby書きが躓いた小石集

これは，[Crystal Advent Calendar 2015](http://www.adventar.org/calendars/800) 12月7日分の記事です。

# はじめに

Ruby の文法に強くインスパイアされ，シンプルなものであれば Ruby スクリプトがそのまま動いてしまうこともある [Crystal](http://ja.crystal-lang.org) ですが，それだけに思わぬところで Ruby の流儀が通用しなくて戸惑うことがあります。

例えば，[暗黙の文字列変換を指定しようとして`#to_s`をオーバーライドしても予想通りの動きをしない](http://qiita.com/arcage/items/a8afc7449966ef7674f9)とか，[初期化のタイミングによってインスタンス変数がコンパイラに`nilable`だと判断される](http://qiita.com/arcage/items/056a42f2a4a2d5690b4c)などなど。

たいていの問題は[ドキュメント](http://crystal-lang.org/docs/)や[APIリファレンス](http://crystal-lang.org/api/index.html)をじっくり読むと解決したりするのですが，縁あって「Crystal Advent Calendar 2015」にお誘いいただいたので，個人レベルで数年に渡って日曜プログラミングとしてRubyと付き合ってきた人間が，Crystal で[個人使用のライブラリ](https://github.com/arcage/crystal-ipaddr) を作る際に躓いた小さなポイントをいくつかまとめてみました。

_（本記事はCrystal 0.9.1，およびRuby 2.2.3を基に書かれています）_

# 実行時の値の型 と compile-time type

Rubyだと変数は「使用する時点でどんな値なのか」がすべてなので，その値がどういった経緯で得られたものであるかは考慮されません。なので，パラメータによって数値か文字列を返すメソッドを用いた以下のコードも問題なく動作します。

```rb:RubyだとOK
def foo(return_int)
  return_int ? 1 : "1"
end

bar = foo(false)  #=> "1"
bar.size          #=> 1
```

一方，上記のコードを Crystal で実行しようとすると，以下のようなエラーが発生します。

```:Crystalだとエラー
undefined method 'size' for String (compile-time type is (String | Int32))
```

Crystal はコンパイル時に各メソッドに対して，「返り値として取りうる型」を調べるようで，複数の型返す可能性があるメソッドの返り値は取りうる全ての型の合成型（union type）になります。そして合成型の変数は，何らかの方法（`if bar.is_a?(String)`とか）で変数の型を特定しない限り，合成元の全ての型が共通して持っているメソッドしか使用できません。

上記コード内では`bar`に対するのメソッド`foo()`の呼び出しは必ず`String`を返しますが，`foo()`自体は`Int32`と`String`を返す可能性があるため，コンパイル時に`bar`は`Int32`と`String`の合成型である`(Int32 | String)`として解釈され，`Int32`には存在しない`#size`の呼び出しはエラーになってしまいます。

エラーメッセージの後に`(compile-time type is (String | Int32))`とヒントを出してくれてはいますが，はじめは「`String#size`が存在するにもかかわらず，それと矛盾するメッセージが出る」という状況にかなり混乱したものです。

この状態の `bar` に対して `bar.class` を確認すると `String` だと返ってくるのも混乱に拍車をかけます。確かにこの時の`bar`は`String`の値を持っているのですが，「実行時に変数が持っている値の型」と「コンパイル時にコンパイラが推定する変数の型」は必ずしも一致ないという点には注意が必要です。後者を調べたい場合，`bar.class`ではなく`typeof(bar)`を使うと目的の情報が得られます。

```rb
bar.class    #=> String
typeof(bar)  #=> (String | Int32)
```

# `Foo`と`Foo#class`は同じように使えない

Rubyでは，あるクラス`Foo`があったとして，

```rb
class Foo
  BAR = "bar"
end
```

`Foo#class`によって得られるクラスオブジェクトは，`#is_a?()`の引数として利用したり，はたまた，そこからクラス定数にアクセスしたりと，定数`Foo`と同じように使えます。

```rb:RubyだとOK
foo = Foo.new
foo.is_a?(foo.class) #=> true
foo.class::BAR       #=> "bar"
```

一方，Crystalで同じことをしようとすると，それぞれ以下のようなエラーが発生します。

```
Syntax error in ./src/ipaddr-test.cr:5: expecting token 'CONST', not 'foo'

foo.is_a?(foo.class)
          ^
```

```
Syntax error in ./src/ipaddr-test.cr:6: unexpected token: ::

foo.class::BAR
         ^
```

エラーメッセージを見る限り，`Foo#class`の返り値が`Foo`と異なっているというよりは，構文解析上の問題のようです。

なお，前者は`foo.class == bar.class`で代替可能，後者はクラス定数の代わりにクラスメソッドを定義するなどして同じような使い方ができます。

```rb:`#class`からでもクラスメソッドなら呼び出せる
class Foo
  BAR = "bar"
  def self.bar
    BAR
  end
end
p Foo.new.class.bar #=> "bar"
```

# `String#split` の第２引数

文字列を指定したパターンで分割した配列を返す`String#split`ですが，パターンだけを指定した場合は配列の末尾に存在する空文字列（`""`）が省略されます。

```rb:RubyでもCrystalでも同じ
"a,b,c,,,".split(/,/)  #=> ["a", "b", "c"]
```

Ruby でも Crystal でも`String#split`に第2引数として最大分割数を指定することができますが，この動作が微妙に異なっているので注意が必要です。

いちばん大きな違いは，第2引数に負の数が指定された場合です。Rubyだと末尾の空文字列を省略しない配列が返りますが，Crystalでは分割されない元の文字列１つを含む配列が返ってきます。

```rb
"a,b,c,,,".split(/,/, -1)  #Ruby    =>  ["a", "b", "c", "", "", ""]
                           #Crystal =>  ["a:b:c:::"]
```

また，第2引数として空文字も含めた分割数と同じかそれより大きな数を指定した場合の挙動も微妙に異なっています。

```rb
"a,b,c,,,".split(/,/, 6)  #Ruby    =>  ["a", "b", "c", "", "", ""]
                          #Crystal =>  ["a", "b", "c", "", ""]
```

Crystalでは，なぜか末尾の空文字が１つ少なく返ってくるのです。_（前者はともかく，後者はバグのような気がしないでもありません）_

データ中にカンマを含まないCSVのエントリをカンマでsplitして，返ってきた要素数で簡易なフォーマットチェックしようとしても，第2引数に負の値を指定して`a,b,c,`と`a,b,c`を区別することができません。また，あらかじめ要素数がわかっているので第2引数に4を指定しても，現状では`a,b,c,`に対して`["a", "b", "c"]`が返ってきてしまい，やはり`a,b,c`と区別がつきません。

さしあたり，フォーマットチェックは正規表現などを使って別の方法で行う必要がありそうです。

# メソッドの有無
Crystalの各クラスに定義されているメソッド類は，多くがRubyと同じ名前で定義されていますが，すべてが同じとは限りません。

例えば，同じ動作をする複数のメソッドの多くは片方に統一されています。`String#size`は存在しますが`String#length`は存在しませんし，`Enumerable(T)#map`は存在しますが`Enumerable(T)#collect`は存在しません。

また，なくなったわけではないのですが，`Enumerable(T)#include?`ではなく`Enumerable(T)#includes?`と現在形になっているのも地味に引っかかるポイントかもしれません。`Enumerable(T)#find`とか`Enumerable(T)#reject`なんかは原型のままなので統一感がないようにも思いましたが，よくよく考えると`Object#is_a?`なんかはRubyでも現在形なんですよね。
なんとなく，「レシーバ メソッド名 パラメータ」をそれぞれ「S V O」に当てはめて文章が成り立つ時は現在形の方が自然なのかも，と思ったりします。

# おわりに

というわけで，（ドキュメント類をあまりちゃんと読み込まずに）Crystal に手を出した Ruby 書きが躓いた小石たちの一例でした。

このような細々した躓きポイントはあるものの，クラスやモジュールの構造，メソッドチェーン，イテレータの使い方，ブロック付きのメソッド呼び出しなど，Ruby で使っていた **作法** がほぼそのまま流用できるため，Ruby の経験者が Crystal を習得するのは比較的容易ではないかと思います。

また，静的型付けやネイティブコンパイルによる速度向上以外にも，Crystal ならではのメリットもたくさんあると感じます。例えば，メソッドのオーバーロードが可能なので，パラメータとして複数の型（数値と文字列とか）を受け入れるメソッドなんかは，実装をパラメータの型ごとに分離できてソースがスッキリしますね。

自分は今のところ Ruby の置き換えとしてしか Crystal を使えていませんが，マクロやCバインディングなど Crystal ならではの機能にもチャレンジしてみたいと思います。