次の「FizzBuzzクイズ」とその下の解答例のコード(Ruby版)を読んで、なぜこのような振る舞いが可能か(あるいはこのコードの問題点を考える)ことを通じ、言語(この場合、Ruby)の動作についての知識を問うクイズのようなちょっとした遊びです。「FizzBuzzクイズ」だけでも十分面白いのでよかったら挑戦してみてください。
#FizzBuzzクイズ
(要件がわかりにくいという意見があったので書き直しました)
通常は引数(レシーバー)として与えられた数(1以上の整数)をそのまま返すが、3の倍数が与えられると"Fizz"を返す fizz 、5の倍数が与えられると"Buzz"を返す buzz というメソッドを適当なクラス(あるいはモジュール)に定義せよ。ただし、これらのメソッドは(単独で用いても前述のとおり機能するが)次の例のようにメソッドチェーン形式で組み合わせて用いることができ、その際少々奇妙な動作をするように実装しなければならない。具体的には、それぞれのメソッドが返すべき文字列があれば(複数ならそれらを連結した)文字列を返し、いずれにも当てはまらない場合はレシーバーの数値をそのまま返す。
p 1.fizz #=> 1
p 3.fizz #=> "Fizz"
p 1.buzz #=> 1
p 5.buzz #=> "Buzz"
p 1.fizz.buzz #=> 1
p 3.fizz.buzz #=> "Fizz"
p 5.fizz.buzz #=> "Buzz"
p 15.fizz.buzz #=> "FizzBuzz"
p 15.buzz.fizz #=> "BuzzFizz"
解答に当たっては次の点に注意
- ここで求められるのは、メソッド(あるいはそのチェーン形式での呼び出し)がその返値としてしかるべき文字列(fizz なら "Fizz"、buzz なら "Buzz")や数値を返すように実装されることであり、評価に伴う標準出力等への出力が上記条件を満たすことではない(3.fizz で "Fizz" を出力するが返値は 3 というような fizz では NG ということ)。
- グローバル変数は使わないことが望ましい。
- 余力があれば、7の倍数を与えると"Pezz"を返し、かつ fizz や buzz と同様に振る舞う pezz メソッドの追加拡張を実際に試し、それが最小限のコード変更で可能であるように設計や枠組みの工夫があればなお良い。さらに余力があれば最後の追記にある要件(「~の倍数のとき」というパターン以外のルール)への対応にもチャレンジしてみてください。
p 7.fizz.buzz.pezz #=> "Pezz"
p 21.fizz.buzz.pezz #=> "FizzPezz"
p 35.fizz.buzz.pezz #=> "BuzzPezz"
p 105.fizz.buzz.pezz #=> "FizzBuzzPezz"
p 105.fizz.pezz.buzz #=> "FizzPezzBuzz"
p 105.pezz.buzz.fizz #=> "PezzBuzzFizz"
# 以下も念のため
p 1.fizz.buzz.pezz #=> 1
p 3.fizz.buzz.pezz #=> "Fizz"
p 5.fizz.buzz.pezz #=> "Buzz"
p 15.fizz.buzz.pezz #=> "FizzBuzz"
p 15.buzz.fizz.pezz #=> "BuzzFizz"
p 104.fizz.buzz.pezz #=> 104
- あえてRuby以外の他言語で挑戦する場合、呼び出しに()を伴わせること等は気にしなくてよい。また実装するのは関数で構わない。つまり、メソッドか関数であるかや、そのコールの形式や書式は問わない。だだし、早期結合の言語(具体的には戻り値に複数の型を許さない言語)での挑戦は、個人的にはまったくお薦めできない。
105.fizz().buzz().pezz() #=> "FizzBuzzPezz"
pezz(buzz(fizz(105))) #=> "FizzBuzzPezz"
(pezz (buzz (fizz 105))) #=> "FizzBuzzPezz"
#FizzBuzzクイズ・ヒント
このクイズの難しいところは、たとえば
3.fizz.buzz
を評価する際に、3 が 3 の倍数であるために 3.fizz の返値が "Fizz" になってしまっているため、続けてコールされる buzz が元の数値が 3 であることを知ることができない場合があることです。
したがって解決すべきポイントは、2番目以降にコールされたときの buzz(あるいは順序が前後した場合も想定されるので fizz も)に対して、レシーバー等の引数として与える以外の方法で 3 をどうやって渡してやればよいかということに尽きます。
つまり、「グローバル変数は使わないことが望ましい」との但し書きは完全なネタバレです。ただしそこでもう一工夫を入れて、解答にひとひねりを加えていただければとの考えで書きました。
#FizzBuzzクイズ・Rubyでの解答例
これは前述の FizzBuzzクイズの解答例ですが、本題の「FizzBuzzクイズ」クイズの問題でもあります。どうしてこのコードが動くのか、ぱっと見て分かりますか?
module FizzBuzzQuiz
def concat(str)
''.instance_exec(self){ |n| @n = n; self }.concat(str)
end
def modulo(m)
@n % m
end
def fizz
modulo(3) == 0 ? concat('Fizz') : self
end
def buzz
modulo(5) == 0 ? concat('Buzz') : self
end
end
[Fixnum, String].each{ |c| c.include(FizzBuzzQuiz) }
p 1.fizz.buzz #=> 1
p 3.fizz.buzz #=> "Fizz"
p 5.fizz.buzz #=> "Buzz"
p 15.fizz.buzz #=> "FizzBuzz"
p 15.buzz.fizz #=> "BuzzFizz"
p 7.fizz.buzz #=> 7
module FizzBuzzQuiz
def pezz
modulo(7) == 0 ? concat('Pezz') : self
end
end
p 7.fizz.buzz.pezz #=> "Pezz"
p 21.fizz.buzz.pezz #=> "FizzPezz"
p 35.fizz.buzz.pezz #=> "BuzzPezz"
p 105.fizz.buzz.pezz #=> "FizzBuzzPezz"
p 105.fizz.pezz.buzz #=> "FizzPezzBuzz"
p 105.pezz.buzz.fizz #=> "PezzBuzzFizz"
# 以下も念のため
p 1.fizz.buzz.pezz #=> 1
p 3.fizz.buzz.pezz #=> "Fizz"
p 5.fizz.buzz.pezz #=> "Buzz"
p 15.fizz.buzz.pezz #=> "FizzBuzz"
p 15.buzz.fizz.pezz #=> "BuzzFizz"
p 104.fizz.buzz.pezz #=> 104
#「FizzBuzzクイズ」クイズ・解説
前述のとおり、グローバル変数等(他にもRubyなら文字列にも持たせることができるインスタンス変数、他言語ならスレッドローカル変数など)を介して、2番目以降のメソッドが元の数値を知ることができるように細工を施してやれば、FizzBuzzクイズのコードは比較的簡単に書けます。
問題は但し書きにある、「pezzメソッドの追加拡張を最小限のコード変更で」というところです。最小限ということは1メソッド追加でできればOKなのですが、これがなかなか難しい。fizz、buzz、pezz メソッドは(method_missing等の別の枠組みで解くのでなければ)必ず、数値か文字列かどちらもレシーバーにしてコールされる場合があるからです。
もちろんObject等、両者の共通のクラスに定義してselfのクラスで分岐する(狭義の)抽象データ型のアプローチを取ることもできますが、そこはなんとかひと工夫を加えたいところです。
そこで考えたのが、fizzやbuzzを数値でも文字列でも実行できるコードにする方法です。それらをモジュールに定義しておいて数値と文字列のクラスそれぞれにインポートしてやれば、pezzの拡張は1メソッドの追加で可能になります。
ただ、相変わらずレシーバーで分岐するコードでは面白くないし冗長にもなるので、レシーバーによって振る舞いを変える(多態する)コードにしました。ただ、余りを出す % や、文字列の追加をする << は、双方のクラスですでに別々の振る舞いが割り振られているので何かを仕込む本目的には使えません。そこで、数値にしか定義されていない modulo と、文字列にしか定義されていない concat に着目しました。文字列をレシーバーにして modulo をコールしたときと、数値をレシーバーにして concat がコールされたときをフックしてこの FizzBuzzクイズに必要な動作をするように細工をしてやれば(具体的には引数で渡せない数値を渡す手はずを整えたり、それを受け取ったりできるようにすれば)目的は達成できるはずです。
このアイデアでもうひとつのキーになるのは、モジュールをインポートした際、すでにそのクラスに同名のメソッドが定義されていたら、そのメソッドが優先されるというRubyのモジュールの振る舞いです。もう少し正確にいうと、Rubyのモジュール(に限らずミックスイン機構における仮想クラス等)は、非明示的にインポート先のクラスのスーパークラスとして、その継承パスに挿入されるしくみになっています。したがって、もし当該モジュールに定義されているメソッドと同名メソッドがインポート先のクラスにあると、事前にオーバーライドされているのと同じことになる、つまり(superさえ呼ばなければ)インポートしたメソッドを遮蔽できるというわけです。このルールを使うと、数値向けの concat、文字列向けの modulo のみをそれぞれのクラスで有効化することが可能になります。FizzBuzzQuizモジュールを Integer ではなく Fixnum にインポートしているのはこうした理由によるものです。
#このコードの問題点(あるいは新基軸言語の可能性)
数値や文字列に関係ない concat や modulo を定義してしまうのはいかがなものか?といういうようなまっとうな意見はさておき、多態性を駆使した件のコードですが、はやり可読性は落ちます。何をやっているのか fizz や buzz(そして拡張時の pezz)を読んでもさっぱり分からないというところはコードとしては大問題です。もとより、このようなトリッキーなコードを記述することはクイズの解答に留めておくべきでしょう。
一方で、レシーバーを特定しない新しいプログラミング言語の可能性を、件のコードを眺めていてちょと垣間見たような気がします。あるコンテキスト内に居合せたオブジェクトのうち、適切なオブジェクトがレシーバーとして適切に選択される、生物で言うなら細胞質という物質のごった煮スープのような状態の中で酵素とそれに対応する基質が適切に組み合わされてそれぞれの役割を果たす関係のような、そんな言語があるとちょっと面白いかもしれないなと感じました。
追記: YAGNI な 蛇足
DRY だとの指摘を受けてついカッとなったのでw、後出しジャンケンよろしくここで少しイジの悪い仕様拡張を追加します。
新しい要件は**「さらに、3 を含む数の時は "Aho" を返すメソッド hozz を、pezz のときと同様の方法で拡張せよ」**です。
ドヤ顔でキメようと思ったら、あろうことか自分のコードが POITROAE ゆえに対応できてなかったので恥を忍んで以下に修正版を。^^;
module FizzBuzzQuiz
def concat(str)
''.instance_exec(self){ |n| @n = n; self }.concat(str)
end
def abs
@n.abs
end
def fizz
abs % 3 == 0 ? concat('Fizz') : self
end
def buzz
abs % 5 == 0 ? concat('Buzz') : self
end
def pezz
abs % 7 == 0 ? concat('Pezz') : self
end
end
[Fixnum, String].each{ |c| c.include(FizzBuzzQuiz) }
p 1.fizz.buzz.pezz #=> 1
p 3.fizz.buzz.pezz #=> "Fizz"
p 5.fizz.buzz.pezz #=> "Buzz"
p 7.fizz.buzz.pezz #=> "Pezz"
p 15.fizz.buzz.pezz #=> "FizzBuzz"
p 21.fizz.buzz.pezz #=> "FizzPezz"
p 35.fizz.buzz.pezz #=> "BuzzPezz"
p 105.fizz.buzz.pezz #=> "FizzBuzzPezz"
p 105.pezz.buzz.fizz #=> "PezzBuzzFizz"
module FizzBuzzQuiz
def hozz
abs.to_s.include?("3") ? concat('Aho') : self
end
end
p 13.fizz.buzz.hozz #=> "Aho"
p 3.fizz.buzz.hozz #=> "FizzAho"
p 35.fizz.buzz.hozz #=> "BuzzAho"
p 30.fizz.buzz.hozz #=> "FizzBuzzAho"
p 30.hozz.buzz.fizz #=> "AhoBuzzFizz"
modulo の多態のときのシレっと感(?)がすっかり失われてしまい(abs だと違和感バリバリで「~クイズ」クイズが成立しない…)自分としては非常に残念なのですが、トリッキーさの基本を変えずに追加ルールの多様化に対する柔軟性を飛躍的に増すことはできたので、まあこれでよしとします。