Crystal言語作者がRubyを愛する理由(1)「等しさ」の扱い(翻訳)
この連載記事では、私がRubyで常日頃から気に入っている点を順に扱っていきたいと思います。他の言語では動作が異なるものもあれば、他の言語にはまったくないものもありますが、可能な限り比較を行ってみます。しかしRubyは、プログラマーが期待する直感のど真ん中をいつも射抜いていると感じさせ、私はそこを愛しています。そしてCrystal言語でも同じ良さを得られるように心がけています。
オブジェクト同士の比較
Rubyで2つのオブジェクト同士を==
で比較するとき、直感を裏切ることはほとんどありません。
1 == 1 # => true
1 == 1.0 # => true (数学的にもこうですよね!)
[1, 2] == [1, 2] # => true
ここで肝心なのは、3つ目の配列の比較です。世の中には、このように判定してくれない言語もざらにあります。たとえば、JavaやC#で同じ比較を行うと、2つの配列が同一の場合(つまりメモリ上の同じ場所を参照している場合)にしかtrue
になりません。"how to compare two arrays in Java/C#"でググってみると、Stack Overflowで山ほど回答が見つかります。Javaの場合はArrays.equals
を使えますが、これはディープな比較を行わないので、そういう場合のためにArrays.deepEquals
もあります。
配列同士の比較は、ある配列が持つオブジェクトが同じ配列への参照を持つと循環参照が発生する可能性がある点が厄介です。比較で循環参照に配慮しておかないと、プログラムはスタックオーバーフローでクラッシュします。こんな結果を望む人はいませんよね。
しかしRubyでは配列同士を問題なく比較できます。2つの配列を比較するときは、各要素に対して==
を呼び出しますが、このとき循環参照にも配慮してくれるので、以下は問題なく動きます。
a = []
a << a # => [[...]]
a == a # => true
上のコードはすべてCrystalでもそのまま動きますが、配列の再帰的な定義についてはこの限りではありません。なお私はCrystalでは問題なく動くと思っていたのですが、どうやらスタックオーバーフローになるようです。これについては最終的に修正するつもりです(#11837)。
Rubyでは、Hash
やSet
など、等しさの定義が自明なさまざまな型についても比較を行えますが、Crystalでも同様です。
等しさはいつでもどこでもチェックできる
Rubyにおける「等しさ」のもうひとつ良い点は、デフォルトが常識的なおかげで任意の2つのオブジェクトをすぐ比較できることです。RubyのStruct
型ではいくつかの値をまとめて、複数のプロパティを持つオブジェクトを手軽に定義できます。
Point = Struct.new(:x, :y)
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p1 == p2 # => true
Crystalでも同様にstructを定義できます。デフォルトの比較は、単にフィールド同士の比較に委譲されます。
struct Point
def initialize(@x : Int32, @y : Int32)
end
end
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p1 == p2 # => true
これらはいずれも実に直感的に理解できます。他の言語がそうしない理由はわかりません。たとえばHaskellでは、このような構造体が多用されます。
❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> data Point = Point { x :: Int, y :: Int }
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p2 = Point { x = 1, y = 2 }
Prelude> p1 == p2
<interactive>:4:1: error:
- No instance for (Eq Point) arising from a use of '=='
- In the expression: p1 == p2
In an equation for 'it': it = p1 == p2
HaskellではPointに==
が定義されていないのでコンパイルエラーになります。エラーにならないようにするには、Pointに==
を定義する必要があります。deriving
というマジックでコンパイラに明示的に同じことをさせることも可能です。
❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> data Point = Point { x :: Int, y :: Int } deriving (Eq)
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p2 = Point { x = 1, y = 2 }
Prelude> p1 == p2
True
最後の部分については賛否両論があります(もしかするとこの記事全体が賛否両論かもしれませんが)。たぶんこんなことを言われそうです。
デフォルトではこの比較を行いたくない場合はどうする?それは危険だ(つまりコードにバグがある)。こうしたケースを考慮して、この
==
が本当に必要かどうか常に確認する方がよいのではないか。
ごもっともです。私の回答は、自分のコードが期待どおりに動いているかどうかはテストで確かめる必要があるということです。コンパイル時に安全が保証されるのはよいことですが、それによってコードの振る舞いまで証明されるわけではありません。
次回予告
次回はRubyのto_s
メソッドを取り上げます。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。