Crystal は配列内要素に対してすべての要素が持つメソッドしか呼び出せなくてつらい

これは「Ruby脳にはCrystalつらい Advent Calendar 2015」の8日目の記事です。

qiita.com

Ruby だと、プログラマーが配列の1番目の要素は整数で、2番目の要素が文字列で…といったように決めて、次のようなコードを書いたりすることがあります。

[
  [ 1, "hoge" ],
  [ 2, "fuga" ],
  [ 3, "piyo" ],
].each do |obj|
  num, str = obj
  num + 100
  str.size
end

Crystal だとこれはコンパイルエラーになります。

% crystal hoge.cr
Error in ./hoge.cr:1: instantiating 'Array(Array(String | Int32))#each()'

[
  ^~~~

in /opt/crystal/src/array.cr:774: instantiating 'each_index()'

    each_index do |i|
    ^~~~~~~~~~

in /opt/crystal/src/array.cr:774: instantiating 'each_index()'

    each_index do |i|
    ^~~~~~~~~~

in ./hoge.cr:1: instantiating 'Array(Array(String | Int32))#each()'

[
  ^~~~

in ./hoge.cr:7: no overload matches 'String#+' with types Int32
Overloads are:
 - String#+(other : self)
 - String#+(char : Char)

  num + 100
      ^

[ 1, "hoge" ] として作られた配列は、「要素が Int32 または String である配列」となります。 その要素に対するメソッド呼び出しは、どちらのクラスでも呼び出すことができるメソッドでなければいけません。 String に 100 を引数とした + メソッドは呼び出せないし、Int32 に size メソッドはありません。 1番目の要素が Int32 で 2番目が String というプログラマの勝手な思いは Crystal には届きません。慈悲はない。

このような場合は、Int32 と String のメンバー変数を持つ構造体のようなクラスを使うか、次のように if でクラス判定をすればよいです。

[
  [ 1, "hoge" ],
  [ 2, "fuga" ],
  [ 3, "piyo" ],
].each do |obj|
  num, str = obj
  num + 100 if num.is_a? Int
  str.size if str.is_a? String
end

または、今回の場合は明に型変換をしてもよいかもしれません。

[
  [ 1, "hoge" ],
  [ 2, "fuga" ],
  [ 3, "piyo" ],
].each do |obj|
  num, str = obj
  num.to_i + 100
  str.to_s.size
end