Help us understand the problem. What is going on with this article?

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

More than 5 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を書いてみてください。楽しいですよ。

supermomonga
ヾ(  l   _   l  〃)ノ゙ドン☆
https://darui.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away