Ruby
メタプログラミング

fizzbuzzを本気で解く(前編)

More than 1 year has passed since last update.


fizzbuzzを小難しく解きたい

以前、カジュアルな面談のような場でおもむろにクイズを出題される、ということがありました。

なんてことはないfizzbuzz問題だったのですが、帰り道、フツフツと疑問が湧いてきました。

うーん。あれは一体、何を見ていたのだろう・・

その時は無難なコードを書いて終わったのですが、あれは貴重なアピールの場だったかも知れないなと反省。

こういう局面において、「どんなfizzbuzzを書いたら強いインパクトを残せるのか」という間違った路線を考えた結果、最終的にfizzbuzz専用のDSLを作ることになりました。

(思いのほか長くなったので、DSLは次回)

本当の面接では無難な答えがいいのかもと思いますw

たぶん。まあ、こんな簡単な問題を聞かれることは稀だと思うけど。


自分のコード

最初にぱっと思いついたのはこんな感じ

(MIN_NUM..MAX_NUM).each do |n|

if n % 15 == 0
puts = 'fizzbuzz'
elsif n % 3 == 0
puts = 'fizz'
elsif n % 5 == 0
puts = 'buzz'
else
puts = n.to_s
end
end

ちなみに、MIN_NUMからMAX_NUMまで計算する設定

(MAX_NUM > MIN_NUMは保証されているものとする)

もうちょっとルビー感を出したかったので、大体こんなところで手を打った

MIN_NUM.upto MAX_NUM do |n|

s = ''
s << 'fizz' if (n % 3).zero?
s << 'buzz' if (n % 5).zero?
s = n.to_s if s.empty?
puts s
end

こんなのでよかったのだろうか。

でもまてよ。さらっとmapが使えるのかを見られていたのかもな・・

(MIN_NUM..MAX_NUM).to_a.map { |n|

m = n
n = 'fizz' if (m % 3).zero?
n = 'buzz' if (m % 5).zero?
n = 'fizzbuzz' if (m % 15).zero?
n.to_s
}.each{|s| puts s}

ちょっとトリビアっぽい使い方も見せられたかもしれない。

a = (MIN_NUM..MAX_NUM).to_a.map {|n|

case [n % 3, n % 5].map(&:zero?)
when [true, true]
n='fizzbuzz'
when [true, false]
n='fizz'
when [false, true]
n='buzz'
else
n=n
end
}
puts *a

まっとうに、条件設定と処理を書き分けるとかもいいのかも。

rule = { 15: 'fizzbuzz', 3: 'fizz', 5: 'buzz' }

def fizzbuzz(n)
rule.each.do |m, comment|
return comment if (n % m).zero?
end
return n.to_s
end

(MIN_NUM..MAX_NUM).to_a.each{ |s| puts fizzbuzz(s) }


ワイルドな奴

この辺から段々こじらせてきた。

devidable = -> (n, m) {(m % n).zero?}

fizzbuzz = -> (s, b) {b ? s : nil}
MIN_NUM.upto MAX_NUM do |n|
s = nil
s ||= fizzbuzz.call(devidable.call(n,15), 'fizzbuzz')
s ||= fizzbuzz.call(devidable.call(n,3), 'fizz')
s ||= fizzbuzz.call(devidable.call(n,5), 'buzz')
s ||= n.to_s
puts s
end

共通部分が気になるので、カリー化してリファクタリング。

devidable =-> (m, n) { (n % m).zero? }

db_curried = devidable.curry
db15 = db_curried.(15)
db3 = db_curried.(3)
db5 = db_curried.(5)

MIN_NUM.upto MAX_NUM do |i|
case i
when db15
puts 'fizzbuzz'
when db3
puts 'fizz'
when db5
puts 'buzz'
else
puts i
end
end

余計に長くなった。

まさかの例外処理を使ってみる

MIN_NUM.upto MAX_NUM do |n|

begin
1/(n % 15)
rescue
puts 'fizzbuzz'
next
end
begin
1/(n % 3)
rescue
puts 'fizz'
next
end
begin
1/(n % 5)
rescue
puts 'buzz'
next
end
puts n
end

モンキーパッチとか

module FizzBuzzExtentions

refine Fixnum do
def fizzbuzz
n = self
n = 'fizz' if (self % 3).zero?
n = 'buzz' if (self % 5).zero?
n = 'fizzbuzz' if (self % 15).zero?
n.to_s
end
end
end

module FizzBuzz
using FizzBuzzExtentions
(MIN_NUM..MAX_NUM).each {|s| puts s.fizzbuzz}
end

メソッドputsをラッピングしてみるのも面白いかもしれない

module PutsWrapper

def puts n
if n.is_a? Fixnum
if (n % 15).zero?
super 'fizzbuzz'
elsif (n % 3).zero?
super 'fizz'
elsif (n % 5).zero?
super 'buzz'
else
super n
end
else
super n
end
end
end

Object.class_eval do
prepend PutsWrapper
end
(MIN_NUM..MAX_NUM).each {|s| puts s}

「面倒くせーやつだ」「一緒に働きたくない」と思われること間違いなしでしょうww


選外

無駄に再帰を使ってみる

def fizzbuzzR(n,max)

return if n>max
if (n % 15).zero?
puts 'fizzbuzz'
elsif (n % 3).zero?
puts 'fizz'
elsif (n % 5).zero?
puts 'buzz'
else
puts n.to_s
end
fizzbuzzR(n+1, max)
end
fizzbuzzR(MIN_NUM, MAX_NUM)

全くルビーぽくないコードで逆アピールするとか

n = MIN_NUM

while n<=MAX_NUM do

mod3 = (n % 3) == 0 ? 1 : 0
mod5 = (n % 5) == 0 ? 1 : 0

case 2 * mod5 + mod3
when 0
puts n
when 1
puts 'fizz'
when 2
puts 'buzz'
when 3
puts 'fizzbuzz'
end

n += 1
end

あとは、「3の倍数は各桁の数字同士を足し、これを1桁になるまで繰り返して行くと3、6、9のいずれかになる」という性質や、「5の倍数は1桁目が5,0で終わる」という特徴を利用して、途方もない労力をかけて倍数を判定する方向など・・・(割愛)

というわけで、もっと面白いアイデアがあればぜひご教示くださいませ。

後編はDSLを作ってみたい。


参考

メタプログラミングruby 第二版

この本を読みながら様々な角度からfizzbuzzを書いてみると、色々な技術が身につく気がしました。ええ。たぶん。