続FizzBuzz
FizzBuzzが書いてみたくなったので、また書いてしまいました。つまり、時間の無駄遣いです。
せっかく書いたので、解説してみたいと思います*1。FizzBuzzのコードを御覧ください。
注意: 白魔術が掛かったソースコードなので、プロダクト環境のFizzBuzzライブラリとかにコピペしても、責任は負いかねます。
ソースコード編
class NilClass # nil の + メソッドを定義する # 与えられた引数をそのまま返す def +(obj) obj end end class String # + メソッドを再定義するために、 # old_plus に + のエイリアスをはる alias :old_plus :+ # 文字列にnilを足した場合に文字列自身を返すよう、 # + メソッドを再定義する def +(obj) obj ? old_plus(obj) : self end end class Integer # i で割り切れるか判定する def devide?(i) self % i == 0 end end # ここまでおまじない # FizzBuzz ここから class Integer # FizzBuzz本体 def fizzbuzz (devide?(3) ? 'fizz' : nil) + (devide?(5) ? 'buzz' : nil) || self end end # 1から100までfizzbuzzを実行して表示する。 1.upto(100).each{|i| puts i.fizzbuzz}
fizzbuzzメソッドは非常にすっきりしましたね。
3で割り切れるか判定した結果と、5で割り切れるか判定した結果を足しあわせて、それでもnilだったら数字自身を返すというのがよくわかります。
例えば、3.fizzbuzz の場合、'fizz' + nil は 'fizz' になり、最終的に 'fizz' || self で 'fizz' が返ります。
解説編
黒白魔術の説明の前に、オープンクラスについて簡単に触れましょう。先ほどのコードを見ると、
class Integer def devide?(i) self % i == 0 end end
と、
class Integer def fizzbuzz (devide?(3) ? 'fizz' : nil) + (devide?(5) ? 'buzz' : nil) || self end end
で、2回Integerクラスを実装しています。このように、Rubyでは一度classを閉じても、もう一度開き直して実装することができます。これがオープンクラスです。
そもそも、Integer自体が整数を表す組み込みライブラリなので、最初のclass Integerの時点で標準のIntegerクラスを開き直しているのです。
すると、1.devide? や 1.fizzbuzz のように、何の変哲もない整数にdevide?メソッドや、fizzbuzzメソッドを追加することができました。
fizzbuzzメソッド
fizzbuzzメソッドを見ると、devide?(3)やdevide?(5)で場合分けをしてfizzbuzzの文字列を作っています。場合分けをしてみてみましょう。
- 5で割り切れる場合 nil + 'buzz' → 'buzz'
- 3でも5でも割り切れない場合 nil + nil → nil
- 3で割り切れる場合 'fizz' + nil → 'fizz'
- 15で割り切れる場合 'fizz' + 'buzz' → 'buzz'
順番に見ていきましょう。
5で割り切れる場合、3でも5でも割り切れない場合
irb で nil + 'buzz' と実行してみましょう。
NoMethodError: undefined method `+' for nil:NilClass
とエラーが表示されます。+メソッドがないので、実装してみます*2。
nilはNilClassのインスタンスなので、NilClassに+メソッドを追加してみましょう。nil + nil は nil、nil + 'buzz' は 'buzz' が返ってきて欲しいので、足したオブジェクトをそのまま返すように実装します。
class NilClass def +(obj) obj end end
これで大丈夫ですね。
3で割り切れる場合、15で割り切れる場合
今度は、'fizz' + nil をirbで実行してみましょう。
TypeError: can't convert nil into String
と言われてしまいました。nilとの足し算ができるように、Stringクラスの+メソッドを再定義してみましょう*3。
nilを足した場合、文字列をそのまま返すようにして、'fizz' + nil で 'fizz' が返るようにします。一方、'fizz' + 'buzz' は 'fizzbuzz' になってもらわないと困るので、nilでないものを足した場合は、そのまま足し算(文字列連結)をすることにします。
つまり、nilチェックをして、nilでなければ足し算をして、nilならselfを返せばいいです。
class String def +(obj) obj ? self + obj : self end end
でよさそうですが、ダメです。'fizz' + 'buzz'をしようとすると、stack level too deepって言われてしまいます。
def + で+メソッドを再定義しています。そのため、+メソッド内で使っている + では、再定義した+メソッドを使ってしまっています。つまり、無限ループが発生してしまいます。
alias :old_plus :+
とすると、old_plus メソッドは + メソッドと同じ動作をするようになります。+ メソッドを再定義しても old_plus メソッドはもとの + メソッドと同じ動作をし続けます。これを利用すると次のようになります。
class String alias :old_plus :+ def +(obj) obj ? old_plus(obj) : self end end
- メソッドの中ではold_plusを使っているので、無限ループの心配はなくなりました。