[Crystal] クラス定義周りの便利マクロ

<i class="fa fa-warning" style="font-size:1em;"></i> <a href="#complements">追記</a>あり

[Crystal Advent Calendar 2015](http://www.adventar.org/calendars/800)のネタを書くにあたって，改めて[ドキュメント](http://crystal-lang.org/docs/)や[APIリファレンス](http://crystal-lang.org/api/index.html)を読み返してみると，クラス定義周りのマクロが非常に充実していることに気づいたので，備忘録代わりにまとめてみました。

#<i class="fa fa-cubes" style="font-size:1em;"></i> はじめに

自作クラスを定義しようとすると，お約束のように定義することになるメソッドがいくつか存在します。
インスタンス変数に対するアクセサなどはRubyでも専用の書式が用意されていましたが，Crystalではそれだけでなく等価演算子 `#==`，`Hash`キーの同一性チェックに使用される`#hash`，さらにはインスタンス変数へのメソッドの移譲についてまでマクロが用意されています。

#<i class="fa fa-cubes" style="font-size:1em;"></i> アクセサ定義

アクセサ定義系のマクロには，対象とするインスタンス変数名をシンボルリテラル（`:name`），文字列リテラル（`"name"`），もしくは名前ベタ書き（`name`）で指定します。

```rb:全部同じ
getter :name
getter "name"
getter name
```

```rb:１回に複数指定可
getter :name, :age
```

_（以下の説明は，各マクロに `:name` が指定された場合を想定します）_

##<i class="fa fa-code" style="font-size:1em;"></i> getter, getter!, getter?

Rubyの`attr_reader`に相当するのが`getter`マクロです。`getter`マクロによってインスタンス変数の値を取得するためのアクセサメソッドが定義されますが，Crystalには`getter`だけでなく`getter!`と`getter?`という派生版も用意されています。

素の`getter`マクロでは，インスタンス変数が`nil`だとしてもそのまま返す`#name`が定義されます。

```rb
class Person
  getter :name
end
john = Person.new
john.name #=> nil
```

`getter!`でも，`#name`は定義されますが，`getter!`によって定義された`#name`はインスタンス変数`@name`が`nil`（もしくは未定義）の場合には例外を挙げます。その代わり，インスタンス変数`@name`が`nil`でもそのまま値を返す`#name?`も同時に定義されます。

```rb
class Person
  getter! :name
end
john = Person.new
john.name  #=> Error: Nil assertion failed (Exception)
john.name? #=> nil
```

最後の`getter?`では`#name`は定義されず，`getter!`の場合と同様の`#name?`のみが定義されます。

```rb
class Person
  getter? :name
end
john = Person.new
john.name  #=> Error: undefined method 'name' for Foo
john.name? #=> nil
```

個人的には`#name?`が定義されるのであれば，`getter!`の動作がデフォルトでも良いんじゃないかと思ったりします。

##<i class="fa fa-code" style="font-size:1em;"></i> setter

Rubyの`attr_writer`に相当するのが`setter`マクロです。`setter`マクロによって，インスタンス変数の値を更新するためのアクセサメソッド`#name=(new_name)`が定義されます。こちらは特に派生はありません。

```rb
class Person
  setter :name
end
john = Person.new
john.name = "John"
```

今のところ`setter`マクロで作成される`#name=`には引数の型を指定できないようで，文字列（`String`）で初期化されたインスタンス変数を数値（`Int32`）で更新するようなことができてしまいます。
Crystalでは，こうした場合どんどん更新後の値の型が合成されていいく（`(String | Int32)`）ことになるため，思わぬところで「合成元すべての型が有するメソッドしか使用できない」という合成型の制約にぶつかる場合があります。

特定の型のみを想定するインスタンス変数については，セッタメソッドは個別に定義した方が安全かも。

##<i class="fa fa-code" style="font-size:1em;"></i> property, property!, property?

Rubyの`attr_accessor`に相当するのが`property`マクロです。同じインスタンス変数に対して`getter`と`setter`を１度で指定できます。`property`にも，`getter`側の動作によって`property!`，`property?`という派生があります。

# <i class="fa fa-cubes" style="font-size:1em;"></i> 同一性定義

##<i class="fa fa-code" style="font-size:1em;"></i> def_equals

Crystal で自作のクラスを作る際，固有の`#==`メソッドを定義しないと，どんな状態のインスタンス同士を比較しても結果は`false`になります。オブジェクトの状態はインスタンス変数によって表現されるので，あるオブジェクトと別のオブジェクトが等しいかかどうかを判断するということは，たいていの場合インスタンス変数（場合によってはその一部）を比較することに他なりません。てなわけで，Crystalには`dec_equals`マクロが用意されています。

`def_equals`マクロにインスタンス変数名のリストを指定すると，与えられたインスタンス変数がすべて等しい場合に`true`を返す`#==(other : self)`が定義されます。

```rb
class Person
  def initialize(@name, @age)
  end
  def_equals @name
end
john = Person.new("John", 20)
ken = Person.new("Ken", 20)
john2 = Person.new("John", 18)
john == ken    #=> false
john == john2  #=> true
```

このマクロの興味深いところは，該当のクラスに`def_equals`マクロで指定したインスタンス変数（この場合`@name`に対するゲッタメソッドが宣言されている必要がなく，また`def_equals`マクロによってもゲッタメソッドが定義されるわけではない点です。

```rb:def_equalsを行っても#nameは未定義
puts john.name #=> Error: undefined method 'name' for Person
```

同じことをマクロを使わずに自分で書こうとすると，何らかの形で外部からインスタンス変数の値を参照可能にする必要がありますが，~~このマクロを使う限りにおいてはインスタンス変数を隠蔽したまま等価演算子を定義することができるようです。~~（どうやらそうでもないっぽい。<i class="fa fa-warning" style="font-size:1em;"></i> <a href="#complements">追記</a>参照）

マクロすごいね!!

ちなみに，クラスではなく構造体（`struct`）を定義する場合，すべてのインスタンス変数が等しい場合に`true`を返す`#==`がデフォルトで用意されていますので，「一部のインスタンス変数を等価判別の対象から外したい」とかでない限りそもそもこのマクロを使う必要がありません。

##<i class="fa fa-code" style="font-size:1em;"></i> def_hash

等価演算子`#==`が`true`を返してくるだけでは`Hash`のキーとしては同値とはみなされません。

```rb
class Person
  def initialize(@name, @age)
  end
  def_equals @name, @age
end
john = Person.new("John", 20)
john2 = Person.new("John", 20)
hash = {john => true}
john == john2         #=> true
hash.has_key?(john2)  #=> false
```

`Hash`でのキーの判別には，`#==`の他に`#hash`が使用されます。（まず`#hash`で求めたハッシュ値が同じかどうか篩いにかけてから，同じハッシュ値でも`#==`が成り立たなければ同一でないとみなす，みたいなイメージ？）

ところが，独自クラスのインスタンスの場合，`#hash`のデフォルトの返り値はそれぞれのオブジェクトに個別に与えられる`#object_id`の値（`UInt64`）となっており，`#==`による等価が成立したとしても`#hash`は同じ値になりません。

```rb
john.hash   #=> 4504120832
john2.hash  #=> 4504120800
```

そのため，オブジェクトを`Hash`キーとして使用するためには`#hash`を再定義する必要があるのですが，インスタンス変数が数値だけであればまだしも，文字列や他のオブジェクトが絡んでくるとどうやって`Int`に丸め込んだものか意外と悩みドコロだったりします。
[`Object#hash`のドキュメント](http://crystal-lang.org/api/Object.html#hash-instance-method)には「`a == b`であれば`a.hash == b.hash`でなければならない」みたいなことも書かれてますしね。

こんな時，`def_equals`と同じように`def_hash`マクロを使用すれば，指定したインスタンス変数からハッシュ値を計算する`#hash`を定義してくれて非常に便利。

```rb
class Person
  def initialize(@name, @age)
  end
  def_equals @name, @age
  def_hash @name, @age
end
john = Person.new("John", 20)
john2 = Person.new("John", 20)
hash = {john => 1}
john == john2         #=> true
hash.has_key?(john2)  #=> true
```

なお，クラスではなく構造体（`struct`）を定義する場合，やはりすべてのインスタンス変数からハッシュ値を算出する`#hash`がデフォルトで用意されてますので，改めて`def_hash`マクロを使用する必要はありません。

##<i class="fa fa-code" style="font-size:1em;"></i> def_equals_and_hash

`#hash`を再定義したいときって，基本的に`#==`とセットで使われることが多く，`#hash`と`#==`で使用されるインスタンス変数も基本的に同じ組み合わせになるはずです。

というわけで，両者を一度に指定できる`def_equals_and_hash`マクロが用意されています。

```rb
class Person
  def initialize(@name, @age)
  end
  def_equals_and_hash @name, @age
end
```

たいていの場合はこちらを使っておけば問題ないのではないかと。

# <i class="fa fa-cubes" style="font-size:1em;"></i> メソッド移譲定義

##<i class="fa fa-code" style="font-size:1em;"></i> delegate

`delegate`マクロにメソッド名と移譲先のインスタンス変数を指定するだけで，一部のメソッド呼び出しを特定のインスタンス変数へ簡単に移譲することができます。

```rb
class Person
  def initialize(@name, @age)
  end
  delegate downcase, @name
end
john = Person.new("John Doe", 20)
john.downcase  #=> "john doe"
```

##<i class="fa fa-code" style="font-size:1em;"></i> forward_missing_to

別のオブジェクトのラッパクラスを書くような場合には，ラッパ側で定義されていないメソッドをすべてインスタンス変数へ移譲したいことがあります。こうした際に`delegate`マクロでメソッドを１つずつ指定することもできますが，ラップ元の型を変更したりするとすべて手直しが必要になってしまいます。

そんな時は，`forward_missing_to`マクロを使用すると，未定義のメソッドが呼ばれた際に，特定のインスタンス変数へ処理を移譲することができるようになります。

```rb
class Person
  def initialize(@name, @age)
  end
  forward_missing_to @name
end
john = Person.new("John Doe", 20)
john.downcase  #=> "john doe"
john.split     #=> ["John", "Doe"]
john.size      #=> 8
```

#<i class="fa fa-cubes" style="font-size:1em;"></i> おわりに

なんというか，Crystalのマクロの可能性を垣間見た感じです。自分で使いこなせるとイロイロできそうな気はするのですが，まだまだちゃんと自分の中のマクロのイメージが固まっていない，というのが正直なところ。

とはいえ，動作原理を完全に理解していなくとも，このクラス定義周りのマクロは便利なものばかりですので積極的に活用していきたいですね。

#<i class="fa fa-warning" style="font-size:1em;"></i> <a id="complements"></a>追記

上では`def_equals`について，ゲッタメソッドが定義されないのでインスタンス変数を外部から隠蔽できる，と書きましたが，どうやら正確ではなかったようです。

確かに，ゲッタとして一般的な形のメソッドは定義されませんが，@ 付きのインスタンス変数そのままの形のゲッタメソッドが暗黙的に定義されるようです。それも，`def_equals`マクロを使用するしないに関わらず，すべてのインスタンス変数に対して。

```rb
class Person
  def initialize(@name, @age)
  end
end
john = Person.new("John Doe", 20)
john.@name  #=> "John Doe"
john.@age   #=> 20
```

ちなみに，通常は未定義のインスタンス変数を参照するとエラーにならず`nil`が取得できますが，クラス宣言内で全く言及されていないインスタンス変数に対して，この暗黙のゲッタを呼ぶと普段あまり見かけないエラーが発生しました。

```rb
john.@sex   #=> Error: Person doesn't have an instance var named '@sex'
```

いずれにしろ，（少なくとも現バージョンの）Crystalではインスタンス変数はすべてpublic状態で参照可能，ということのようです。

この仕様，カプセル化とか考えるとなんとなくモヤモヤする動きですね。

あと，やはりというか何というか，`def_equals`マクロで指定するのはインスタンス変数でなくとも，public なメソッドなら何でもOKみたい。（private なメソッドや，存在しないメソッドを指定するとエラーが発生する）

```rb
class Person
  def_equals name_and_age

  def initialize(@name, @age)
  end

  def name_and_age
    "#{@name}/#{@age}"
  end
end
john = Person.new("John Doe", 20)
john2 = Person.new("John Doe", 20)
john == john2  #=> true
```

そもそもが`def_equals`マクロで指定していたのは，インスタンス変数ではなく public な暗黙のゲッタメソッドだったわけで，だったら，暗黙のゲッタメソッドとか使わないで明示的に public として宣言したメソッドを指定するようにしちゃダメなのかな？（存在しないメソッドや private なメソッドを指定すればそのようにエラーが出るんだし）

と思ったけど，そうすると構造体（`struct`）でデフォルトの`#==`が定義できなくなるのか……。

Ruby だと構造体の各要素には自動的に public なゲッタメソッドが定義されてけど……。

う〜ん，色々と悩ましいですね。

-- [Crystal-JP Slack](http://crystal.pine.moe/)で指摘していただいたpacuumさんに感謝します。