Ruby: FizzBuzzでいろいろ遊んでみた

初めまして、hiraです。
弊社に入社してからもう7ヶ月も経っていますが、TechRachoの記事を書くのはこれが初めてです。
アドベントカレンダーということで、ないコンテンツを絞って書いてみました。

FizzBuzz

さて今回書く内容は、FizzBuzzです。
プログラマなら大抵ご存知と思いますが、簡単に内容に触れておくと、プログラマの技術力を測るための問題で、以下のような内容です。

「1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリント し、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。」 (「どうしてプログラマに・・・プログラムが書けないのか?」より引用)

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, ...
みたいな出力が得られればOKです。区切り文字は本質ではないのでなんでもよいです。私は改行で実装しています。
また、Fizzだけだとあっているかわかりにくいので、以降3: Fizzのような形で出力するようにします。

普通に解く

前書きはこれくらいにして早速解いていきましょう。
私なら次のように書きます。

MAX = 30

def fizz_buzz(n)
  ret = ''
  ret = 'Fizz' if n % 3 == 0
  ret += 'Buzz' if n % 5 == 0
  ret.empty? ? n.to_s : "#{n}: #{ret}"
end

1.upto(MAX) { |i| puts fizz_buzz(i) }

実行すれば以下のようになるはずです。

1
2
3: Fizz
4
5: Buzz
6: Fizz
7
8
9: Fizz
10: Buzz
11
12: Fizz
13
14
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: Fizz
22
23
24: Fizz
25: Buzz
26
27: Fizz
28
29
30: FizzBuzz

次のように4パターンに場合分けする方も多くいらっしゃると思います。速度は文字列の連結分、上のコードより速いらしいです。ですが、以降の拡張性を考えて上のやり方で進めます。

MAX = 30

def fizz_buzz(n)
  if n % 15 == 0
    "#{n}: FizzBuzz"
  elsif n % 3 == 0
    "#{n}: Fizz"
  elsif n % 5 == 0
    "#{n}: Buzz"
  else
    n.to_s
  end
end

オブジェクト指向的

さてこれらのコードだと、「オブジェクト指向でない!」という声がどこからか飛んできそうです。
ということで、Integerクラスに#to_fizz_buzzを追加してみましょう。

MAX = 30

class Integer
  def to_fizz_buzz
    ret = ''
    ret = 'Fizz' if self % 3 == 0
    ret += 'Buzz' if self % 5 == 0
    ret.empty? ? self.to_s : "#{self}: #{ret}"
  end
end

1.upto(MAX) { |i| puts i.to_fizz_buzz }

どうせなら条件式もメソッドにしましょう。

MAX = 30

class Integer
  def to_fizz_buzz
    ret = ''
    ret = 'Fizz' if self.multiple?(3)
    ret += 'Buzz' if self.multiple?(5)
    ret.empty? ? self.to_s : "#{self}: #{ret}"
  end

  def multiple?(n)
    n.kind_of?(Integer) && self % n == 0
  end
end

1.upto(MAX) { |i| puts i.to_fizz_buzz }

出力の部分は、

puts (1..MAX).map(&:to_fizz_buzz).join("\n")

でもよいのですが、これだとMAX = Float::INFINITYにしたとき、mapで止まってしまうのが不満です。

一般化

このまま普通のFizzBuzzを続ける手もありますが、結構手垢のついたネタなので、これぐらいで切り上げて、一般化をしてみます。
3, 5とくれば普通に考えると7, 11, 13と素数列が続きそうです。
例えば、FizzBuzzHogeは3の倍数がFizz, 5の倍数がBuzz, 7の倍数がHogeということで、
1, 2, 3: Fizz, 4, 5: Buzz, 6: Fizz, 7: Hoge, ... , 21: FizzHoge, ..., 105: FizzBuzzHoge
となりそうです。
素数は自動的に決定できるので、0個以上の文字列をコンストラクタが受け取るようなGeneralFizzBuzzクラスを作ればよさそうです。(FizzBuzzHogeの場合'Fizz', 'Buzz', 'Hoge'をコンストラクタに渡す)
とすると、GeneralFizzBuzzクラスは次のように書けます。

require 'prime'

class GeneralFizzBuzz
  def initialize(*keywords)
    @keywords = keywords
  end

  def execute(max)
    1.upto(max) { |n| puts output(n) }
  end

  def output(n)
    ret = ''
    @keywords.zip(Prime.lazy.reject { |prime| prime == 2 }).each do |keyword, prime|
      ret += keyword if n % prime == 0
    end
    ret.empty? ? n.to_s : "#{n}: #{ret}"
  end
end

@keywords.zip(Prime.lazy.reject { |prime| prime == 2 })は文字列の配列と3以上の素数列をzipしています。Primeは無限の長さを持つので、単にrejectしてしまうと処理が無限に継続してしまいます。そのため、Enumerator#lazyを用いてそれを回避しています。

Enumerator#lazyは自身をEnumerator::Lazyオブジェクトに変えたものを返すメソッドで、Enumerator::Lazyは関数型言語でよく用いられる遅延評価を可能にするオブジェクトです。

Prime.lazy.reject { |prime| prime == 2 }の代わりにPrime.take(@keywords.size + 1)[1..-1]としてもよいです。

動かしてみると以下のようになります。

irb > GeneralFizzBuzz.new('Fizz', 'Buzz', 'BPS').execute(30)
1
2
3: Fizz
4
5: Buzz
6: Fizz
7: BPS
8
9: Fizz
10: Buzz
11
12: Fizz
13
14: BPS
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: FizzBPS
22
23
24: Fizz
25: Buzz
26
27: Fizz
28: BPS
29
30: FizzBuzz

ちゃんと7の倍数でBPSが出力されるようになっています。

#method_missing を使って一般化

最後に、かの#method_missingを使ってみたかったので、それを使った実装を紹介して終わります。

require 'prime'

class Range
  def method_missing(method_name)
    keywords = method_name.to_s.split('_').map(&:capitalize)
    GeneralFizzBuzz.new(self, *keywords).execute
  end
end

class GeneralFizzBuzz
  def initialize(range, *keywords)
    @range = range
    @keywords = keywords
  end

  def execute
    @range.each { |n| puts output(n) }
  end

  def output(n)
    ret = ''
    @keywords.zip(Prime.lazy.reject { |prime| prime == 2 }).each do |keyword, prime|
      ret += keyword if n % prime == 0
    end
    ret.empty? ? n.to_s : "#{n}: #{ret}"
  end
end

Rangeの定義されていないメソッドが呼び出されたときに、メソッド名を_で分割し、頭文字を大文字にした上で、GeneralFizzBuzzのコンストラクタに渡しています。 (1..MAX).fizz_buzzで通常のFizzBuzzとなります。

irb > (1..30).fizz_buzz_bps_advent_calendar
1
2
3: Fizz
4
5: Buzz
6: Fizz
7: Bps
8
9: Fizz
10: Buzz
11: Advent
12: Fizz
13: Calendar
14: Bps
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: FizzBps
22: Advent
23
24: Fizz
25: Buzz
26: Calendar
27: Fizz
28: Bps
29
30: FizzBuzz

ちゃんと11の倍数でAdvent, 13の倍数でCalendarが出力できています。
めでたしめでたし。

まとめ

#method_missing#lazyと実務ではなかなか使わないメソッドが使えて案外楽しかったです。
ただ、これを面接で聞かれて最後の方の実装を答えた場合にどんな顔をされるのかは少々気になるところです。
また、これ以外にも、ラムダ式やカリー化を使って色々できると思うので、皆さんも興味があればやってみてください。

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hira

hiraの書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ