Ruby
FizzBuzz

Rubyによる不適切なFizzBuzzの世界

More than 3 years have passed since last update.

最近不適切にFizzBuzzをやるのが楽しく、常にFizzBuzzのことばかり考えている気がします。これが"恋"というものなのでしょうか。

というわけなので、最近書いたFizzBuzzをいくつか紹介したいと思います。

basic.rb 基本パターン

basic.rb
(1..100).each do |n|
  puts case 0
  when n % 15 then :FizzBuzz
  when n % 3 then :Fizz
  when n % 5 then :Buzz
  else n
  end
end

いいですね。ポイントはcaseの使い方です。caseは、caseに渡したオブジェクトと、各when節の評価結果を===メソッドで比較します。例えばFizzBuzzに該当するかの判定部分は0 === n % 15といった処理が走ることになるわけですね。

tap.rb tap with breakの活用

tap.rb
(1..100).map{|_|
  _.tap{|_|
    break :FizzBuzz if _ % 15 == 0
    break :Fizz if _ % 3 == 0
    break :Buzz if _ % 5 == 0
  }
}.map &method(:puts)

tapは基本的にブロックの評価値を捨てますが、breakした場合に限り副作用を与えます。それを利用し、FizzBuzz, Fizz, Buzzのいずれかの条件に当てはまる場合のみbreakしています。

default_value.rb Hashのデフォルト値を利用

default_value.rb
(1..100).map(
  &->_{{0=>:FizzBuzz,3=>:Fizz,5=>:Buzz,6=>:Fizz,9=>:Fizz,10=>:Buzz,12=>:Fizz}.fetch _%15,_}
).map &method(:puts)

Hash#fetchは、第一引数で指定したキーがハッシュに存在すれば対応する値を、存在しなければ第二引数で指定した値を返すメソッドです。任意の整数nを15で除算した剰余が取りうる値は0..14なので、その中で FizzBuzz, Fizz, Buzzに該当する0,3,5,6,9,10,12をキーとして持つHashを作成し、fetchメソッドを利用することで任意の整数についてFizzBuzzの判定を行う事ができます。

php.rb PHPとしても実行可能なFizzBuzz

php.rb
p <<'PHP_VERSION;'
<?php
PHP_VERSION;
print "\033[1F\033[1M";
//.tap{ define_method :range, -> s,e { s.upto e } }
//.tap{ define_method :array_map, -> f,seq { seq.map{ |x| f.(x) } } }
//.tap{ define_method :function, -> x,&b { -> x { $x=x;b.call } } }
array_map(function($x){
  print $x % 15 == 0 ? 'FizzBuzz' : ($x % 3 == 0 ? 'Fizz' : ($x % 5 == 0 ? 'Buzz' : $x));
  print "\n";
}, range(1,100));

phpとしてもrubyとしても実行できるFizzBuzzです。php.rbの詳細についてはPHPとしても実行できるRubyの書きかたをどうぞ。

method_missing.rb method_missing と define_method の活用

method_missing.rb
module Kernel
  define_method '0', -> idx, _ { %w`Fizz Buzz FizzBuzz`[idx] }
end
def method_missing n, _ = nil, orig = nil
  v, i = [3,5,15].map{|_|n.to_s.to_i % _}.each_with_index.sort_by{|_|[_[0],_[1]*-1]}.first
  return _ ? orig : send(v.to_s, i, n)
end
(1..100).map(&:to_s).map(&method(:send)).map(&:to_s).map(&method(:puts))

このFizzBuzzは数そのものをメソッド名として扱いコールすることで目的の値を得るというコンセプトのものです。method_missingが再帰呼び出しされ、最終的に目的の値に到達します。目的の値の取り得る値は、呼び出しを試みたメソッド名そのもの,:FizzBuzz,:Fizz,Buzzのいずれかです。

まず1〜100までの整数を文字列に変換し、その文字列をメソッド名としてKernelsendしています。sendメソッドは、引数で指定したメソッドをレシーバにおいて実行させるメソッドです。例えばKernel.send :fooKernel.fooと同義です。

そのようにしてKernel.1からKernel.100までのメソッドが呼び出されるのですが、そもそも数字から始まるメソッド名はvalidではないので、通常は定義できませんし呼び出すこともできないのですが、sendメソッドを経由することで呼び出すことが可能です。

しかし、当然それらのメソッドは定義されていないので、method_missingが呼ばれることになります。このmethod_missingでは、呼び出されたメソッド名をto_iすることでFixnumに変換した上で、3,5,15それぞれにて除算を行った際の剰余の中で最小のものを変数vに、vが0の場合は、剰余が0になった除算に応じたインデックス番号をiに代入します。

その後、method_missingの引数_nilでない場合はorigの値を返すのですが、そうでない場合Kernelに対してv, i, nを引数としてsendを行います。method_missingの引数_のデフォルト値はnilなので、vの値に応じてmethod_missingが再帰するか、あらかじめKernelに定義しておいた0メソッドに到達することになります。method_missingが再帰呼び出しされた場合、その際の第二引数にはnilでない値が渡されるので、method_missingによって再帰呼び出しされたmethod_missingは必ず第三引数origを返却します。第三引数origには元々method_missingが呼び出された時のメソッド名が渡されているので、結果的に3,5,15いずれの数で除算した場合も剰余が0にならない数をメソッド名として呼び出しした場合は、メソッド名そのものがそのまま返却されることになります。逆に剰余が0になる数をメソッド名として呼び出しした場合は、最終的にKernel.0メソッドに到達します。Kernel.0メソッドは渡されたインデックス番号に従って:FizzBuzz,:Fizz,:Buzzいずれかの値を返すメソッドです。数字から始まる名前を持つメソッドは本来定義できませんが、define_methodを利用することで定義することが可能です。

ascii_art.rb アスキーアートから生成するFizzBuzz

ascii_art.rb
__,__,f,i,z,z,__,__,b,u,z,z,__,__=DATA.map(&method(:eval)).map &:size
F        = (f* i+z+z+b+u+z+z) *(i**i*i-i**z)/i
  I      = (f**i+z+z+b+u+z+z) +i**i+i**z
    Z    = (f**i+z+z+b+u+z+z) +f*i+i
      Z
B        = (f* i+z+z+b+u+z+z) *(i**i*i-i**z)/i-i**i
  U      = (f**i+z+z+b+u+z+z) +f*i-i-i**z
    Z
      Z
((([[f,i,z,z,b,u,z,z].inject(&:*)]*i).inject(&:**))..(f**i+z+z+b+u+z+z)).map{|fizzbuzz|
  case [F,i,z,z,B,u,z,z].inject &:*
  when fizzbuzz %(f+f/i)     then [F,I,Z,Z,B,U,Z,Z]
  when fizzbuzz %(i+i**z)    then [F,I,Z,Z]
  when fizzbuzz %(i**i+i**z) then [B,U,Z,Z]
  else (% %s % fizzbuzz).chars.map(&:to_i).map &(f**i/i-i).method(:+)
  end.pack 'c*'
}.map &method(:puts)
__END__
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%          %%%%          %%%%          %%%%          %%%%%%%%%%%%%%%%%%%%%%%%%
%%  %%%%%%%%%%%%%%%%  %%%%%%%%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%%%%%%%%%%%%%%%%
%%          %%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  %%%%%%%%%%%%%%%%  %%%%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  %%%%%%%%%%%%          %%%%          %%%%          %%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%        %%%%%%  %%%%%%  %%%%          %%%%          %%%
%%%%%%%%%%%%%%%%%%%%%%%%  %%%%  %%%%%%  %%%%%%  %%%%%%%%%%  %%%%%%%%%%%%  %%%%%
%%%%%%%%%%%%%%%%%%%%%%%%  %%      %%%%  %%%%%%  %%%%%%%%  %%%%%%%%%%%%  %%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%  %%%%%%  %%%%  %%%%%%  %%%%%%  %%%%%%%%%%%%  %%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%          %%%%          %%%%          %%%%          %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

このFizzBuzzは一見すると難解に見えますが、タネさえわかってしまえば単純なものです。Rubyでは__END__以降の文字列はFileオブジェクトとしてDATAに渡されるので、DATA.mapにて%および半角スペースで構成された複数の文字列をイテレートすることができます。その文字列に対してevalした結果にsizeメソッドを呼び出し、_,f,i,z,b,u変数にそれぞれ割り当てています。

さて、試しにDATAに渡る内容をいくつかevalした結果を見てみましょう。

eval "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
#=> ""

eval "%%          %%%%          %%%%          %%%%          %%%%%%%%%%%%%%%%%%%%%%%%%"
#=> "          "

eval "%%  %%%%%%%%%%%%%%%%  %%%%%%%%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%%%%%%%%%%%%%%%%"
#=> "  "

eval "%%          %%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
#=> "          "

ご覧のように、0個以上の連続した半角スペースにて構成される文字列が返っています。このからくりは文字列リテラルとString#%メソッドにあります。

%%          %%%%          %%%%          %%%%          %%%%%%%%%%%%%%%%%%%%%%%%%

このコードは以下の様に書き換えることができます。

%%          %.%(
  %%%.%(
    %%  %.%(
      %%%.%(
        %%%.%(
          %%  %.%(
            %%%.%(
              %%%.%(
                %%  %.%(
                  %%%.%(
                    %%%.%(
                      %%%.%(
                        %%%.%(
                          %%%.%(
                            %%%.%(%%%)
                          )
                        )
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )
)

まだわかりにくいですか?それでは更に以下の様に書き換えてみましょう。

%`          `.%(
  %``.%(
    %`  `.%(
      %``.%(
        %``.%(
          %`  `.%(
            %``.%(
              %``.%(
                %`  `.%(
                  %``.%(
                    %``.%(
                      %``.%(
                        %``.%(
                          %``.%(
                            %``.%(%``)
                          )
                        )
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )
)

いいですね。実は%%%%% %は単なる文字列のリテラル、それ以外の%は文字列のフォーマット用メソッドだったのです。%メソッドは演算子のように使えるシンタックスシュガーが導入されており、更に演算子と対象の識別子との間に半角スペースを置く必要がないので、単なる%の羅列の様に見せかけることができるのです。その中に半角スペースを混ぜることでアスキーアートの様にしていたというのが種明かしになります。

あとは簡単ですね。evalによって得られた文字列のsizeによりいくつかの英数を得ることができるので、あとはそれをこねくり回してFizzBuzzを構成するだけです。数字から文字列を作るにはArray#packメソッドを使いましょう。

[70, 105, 122, 122, 66, 117, 122, 122].pack 'c*'
#=> "FizzBuzz"

おわり

以上になります。皆さんもぜひ、不適切なFizzBuzzを書いてみてください。楽しいですよ。