続FizzBuzz

FizzBuzzが書いてみたくなったので、また書いてしまいました。つまり、時間の無駄遣いです。

せっかく書いたので、解説してみたいと思います*1FizzBuzzのコードを御覧ください。

注意: 魔術が掛かったソースコードなので、プロダクト環境の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' が返ります。

えっ? 'fizz' + nil で TypeError が起こるんですか?

解説編

白魔術の説明の前に、オープンクラスについて簡単に触れましょう。先ほどのコードを見ると、

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の文字列を作っています。場合分けをしてみてみましょう。

  1. 5で割り切れる場合 nil + 'buzz' → 'buzz'
  2. 3でも5でも割り切れない場合 nil + nilnil
  3. 3で割り切れる場合 'fizz' + nil → 'fizz'
  4. 15で割り切れる場合 'fizz' + 'buzz' → 'buzz'

順番に見ていきましょう。

5で割り切れる場合、3でも5でも割り切れない場合

irbnil + 'buzz' と実行してみましょう。

NoMethodError: undefined method `+' for nil:NilClass

とエラーが表示されます。+メソッドがないので、実装してみます*2

nilはNilClassのインスタンスなので、NilClassに+メソッドを追加してみましょう。nil + nilnilnil + 'buzz' は 'buzz' が返ってきて欲しいので、足したオブジェクトをそのまま返すように実装します。

class NilClass
  def +(obj)
    obj
  end
end

これで大丈夫ですね。

3で割り切れる場合、15で割り切れる場合

今度は、'fizz' + nilirbで実行してみましょう。

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 + で+メソッドを再定義しています。そのため、+メソッド内で使っている + では、再定義した+メソッドを使ってしまっています。つまり、無限ループが発生してしまいます。

そこで、よく使われるのが*4alias*5です。

alias :old_plus :+

とすると、old_plus メソッドは + メソッドと同じ動作をするようになります。+ メソッドを再定義しても old_plus メソッドはもとの + メソッドと同じ動作をし続けます。これを利用すると次のようになります。

class String
  alias :old_plus :+
  
  def +(obj)
    obj ? old_plus(obj) : self
  end
end
  1. メソッドの中ではold_plusを使っているので、無限ループの心配はなくなりました。

まとめ

FizzBuzzの世界は本当に奥が深いですね。時間の無駄のようでいて、メタプログラミングRubyを読んだ復習にはなっていたので、よかったかもしれません。

みなさんも趣向を凝らしてFizzBuzzを書いてみましょう。

*1:つまり、時間の無駄遣いです

*2:NilClassにメソッドを追加するとか危険なことはやめて、そもそもの実装を見直すことをおすすめします

*3:再定義する前に、標準でnilとの足し算がエラーになる理由を、胸に手をあててよく考えてみましょう

*4:私はaliasを使ったのがこれではじめてです

*5:メタプログラミングRubyの本では、aliasはメソッドではなくキーワードだから、シンボルではなくメソッド名をそのまま書けると念押ししていたので、ここでもチラッと触れてみます