Ruby
FizzBuzz
ポエム
クイズ

「FizzBuzzクイズ」といふものするなり

元ネタ: 「FizzBuzzクイズ」クイズ-Ruby編

まずはコードから

と言うことで私が書いたコードは下記になりました。Ruby 2.4.0以上で実行してください。古いRubyではうまくいかない部分があります。

# frozen_string_literal: true

require 'delegate'

class NamedInteger < DelegateClass(Integer)
  NAME_MAP = {
    fizz: 3,
    buzz: 5,
    pezz: 7,
  }.freeze

  def initialize(n, name = nil)
    super(n)
    @name = name&.-@()
    freeze
  end

  def to_s
    @name || super
  end

  def inspect
    @name&.inspect || super
  end

  NAME_MAP.each do |key, value|
    define_method(key) do
      if (self % value).zero?
        NamedInteger.new(to_i, @name.to_s + key.to_s.capitalize)
      else
        self
      end
    end
  end
end

module IntegerNameable
  refine Integer do
    NamedInteger::NAME_MAP.each_key do |key|
      define_method(key) do
        NamedInteger.new(self).__send__(key)
      end
    end
  end
end

using IntegerNameable

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.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

以下、妄想です。

元ネタで思ったこと

元ネタの回答を見たときに思ったことは三つほどです(三つとはいっていない)。

  1. Stringにインスタンス変数を追加するのはいかがなものなのだろうか?
  2. 同じようなコードが2回以上ある。世の中はDRYでなければならない。
  3. pezz実装に5行(実質3行)は多すぎな気がする。
  4. モンキーパンチと言えばルパン三世だろ。1

これらを無理矢理解決していきたいと思います。

解説っぽい何か

名前付き整数というもの

まずは、一枚目。ドロー、新たなクラス!Integerを委譲して追加メソッド!

冗談です。始めにNamedIntegerを解説します。

さて、3.fizzとしたら"Fizz"な何かになるとの話でした。これは果たして整数なのでしょうか文字列なのでしょうか?"Fizz"と言っているのだから文字列ではありそうですが、その後のbuzzなどのメソッド考えると整数のままとも考えられます。概念的にはどちらが主であるかとすると私は整数を主としました。なぜなら、もともとそれは3だったのだから、相変わらず3であるべきだし、ただ普通の3とは違うのは、ひとたびpとかputsとかしたら"Fizz"になるというだけだからです。

そこで作ったのが名前付き整数クラスNamedIntegerです。この名前付き整数であれば、"Fizz"という名前がついた3が実現できます。もちろん無名の場合もありますが、そのときは通常の3と同じです。何と言ってもその特徴は、Integerを委譲しているので、普通の整数として扱えます。ただ違うのは、to_sinspectです。つまり、文字列にしたいときは名前があれば名前を出すようにしますし、それがinspectで本質も見たいのであれば、やはりそれも名前があれば名前にします。最後に、通常の整数と同じくfreezeしておきます。これが名前付き整数です。

あとは、このクラスにfizzなりなんなりを実装すればメソッドチェーンでどんどん渡せます。

まぁ、ぶっちゃけ私としては、pで確認することがおかしいんじゃないのかと思っていたりもするのですが。

君はDRYなやつだな

世の中にはすごく厳しい人がいます。コードレビュー時に

「この行とこの行で同じ事している、やり直し」

とバスバス切り刻んでくる人です。そういう人をDRYな人と言います2。ということで似たような処理はまとめて書きます。メッソド定義でもなんでも動的にサクサクとできてしまうRubyにかかれば、同じ事をまとめて書くのなんて朝飯前です3Hashでまとめて、eachで回して、define_methodで定義すれば良いだけですから。

DRYなやつのおかげでした

先ほどまとめたおかげで一つ利点が生まれました。pezzについてはたった一行で済むと言うことです。9行目のpezz: 7,が全てです。この後さらに何でも一行で足せます。本当に最小限。

refineとusingが織り成す奇跡を見よ

君はRefinementsの栄光を知っているのか?知らない、ならば今すぐRubyマガジンに書いてある愚痴を読むべきだ。

2.0で追加されるはずだったが先延ばしされて、2.1から正式導入されたのがRefinementsです。これを語るにはRubyの特徴から言う必要があります。

Rubyはとてつもなく動的です。動的型付けだからとかそういう話ではなく、その仕組みが動的なのです。特にクラスは、オープンクラスと言われ、後からいかようにも動作を変えられます。自分で作ったクラスだけでは無く、requireした他人のライブラリは愚か、組み込みクラスですら変えられます。active_supportなんてその力によって実現していることがほとんどです。

この力は非常に強力であるが故に非常に危険です。有名所ではmathnライブラリでしょう。ひとたびmathnをrequireすれば、1/2が½になります。何を言っているのかわからないと思いますが、そういうことです。

今回のクイズでは3.fizzのようにIntegerには元々無いメソッド呼び出しています。Integerそのものに手を加えないことにはこれを実現することはできません。オープンクラスの恩恵により、Rubyでは組み込みクラスであるIntegerですら改造可能です。ですが、これは非常に危険なことです。下手な拡張はどこでどんな副作用が出てくるのか想像もできません。何が起こるかわからないドキドキ感を味わいたいわけでは無いので、他にはなるべく影響が無いようにしたいです。

そこで登場するのがRefinementsであり、それを実現するrefineusingです。適当なモジュールを一つ作って、そこで、refineを使って、拡張したいクラスを拡張します。そして、そのモジュールをusingすると、その場所でのみ、その拡張が有効になるのです。他の場所では、その拡張はなかったことになったままです。

usingはトップレベルとモジュールまたはクラス定義内でのみ使用できます。トップレベルの場合はusingを使った行からファイルの終わりまでのそのファイル内でしか有効になりません。モジュールやクラスもusingを使った行から、モジュールやクラスの定義が終わるまでです。このように、影響が出る範囲を極めて狭くすることができるため、余計な副作用を心配しなくて済みます。

で、結局何?

今回一番難しかったのは、一番最初のNamedIntegerです。正直IntegerizedStringにすべきか1日悩みました。その結果、流行に乗り遅れてしまったので、ポエムを書くしかなくなりました。元ネタは文字列に変わることを本質に捉えているようですが、私としては整数は整数のままであることが本質だと思います。途中愚痴りましたが、pではなくputsで結果が普通のFizzBuzzのようになるというほうが、自由度が出て、意味のあるものだったような気がします。pは本来デバッグ用で、プログラムにおいてはあまり意味のある物ではないですし。

なお、この文章は、「モンキーパッチとモンキーパンチって似ているなー。お、リファインとルパンも似ているような気がする。このネタ使おう」って思い立ってしまったがゆえに書き殴ったただのポエムです。


  1. たぶん、monkey patch(モンキーパッチ)とrefine(リファイン)をかけたダジャレを言いたかっただけだと思う。 

  2. 信用しちゃ駄目だぞ。単にDRY(Don't repeat yourself)原則にかけているだけですから。 

  3. この記事は、夕飯喰って風呂入り終わった寝る前に投稿しているけどな。