Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

ifを使わず、エラーでFizzBuzzを実装してみよう

始めに

FizzBuzz愛好家の皆さんこんにちは。野良FizzBuzz研究家の似非原です。

FizzBuzz研究というのは様々なジャンルがあります[要出典]。例えば、どれだけコードが短く書けるかに注力するCodeGolf派もいますが、一方でさまざまなFizzBuzzを書いて喜びとしている一派があり、それが自分だったりします(確認したところ、自分一人です)。FizzBuzzについては、もうことさら説明する必要もないかとは思いますが、もし知らない人は、適当にGoogleかなにかで検索してくれるとありがたいです。

中級FizzBuzzerの基本教養: if禁止

まず最初に、FizzBuzzの基礎教養として──つまり、FizzBuzz初心者からFizzBuzz中級者になる場合において──まずifを使わずに、どう分岐を表現するのか、というのがあるでしょう。例えばRubyにおいて、ifを使わずにFizzBuzzを実装する例としては、次のようなコードが書けます。

class FizzBuzz
  def initialize(n)
    @n = n
  end

  def fizzbuzz_true_false
    "Fizz"
  end
  
  def fizzbuzz_false_true
    "Buzz"
  end

  def fizzbuzz_true_true
    fizzbuzz_true_false +
    fizzbuzz_false_true
  end

  def fizzbuzz
    eval "fizzbuzz_#{@n%3==0}_#{@n%5==0}"
  end
  
  def method_missing(name)
    @n
  end
  
  def to_s
    fizzbuzz
  end
end

1.upto(100) { |n| puts FizzBuzz.new(n).to_s }

これはRubyのevalを利用することによって、動的にメソッドを呼び出した上で、擬似パターンマッチのようなものを作っています。他の方法も色々あるのですが(ヒントとしては配列を使う)、このようにifを使わないで、分岐を実現するという方法については、初級テクニックといってもいいでしょう。

そこでエラーをreturn代わりに使ってみよう

標準的なFizzBuzzの場合

標準的なFizzBuzzですと、如何のようになります。わかりやすいようにreturnも追加しています。

def fizzbuzz(n)
  if n % 15 == 0
    return "FizzBuzz"
  elsif n % 3 == 0
    return "Fizz"
  elsif n % 5 == 0
    return "Buzz"
  else
    return n
  end
end

このコードの挙動を見てみると、要するに「ある条件にマッチしたら、この関数の返り値を出力する元の関数に戻してあげて、それを出力すれば良い」という構造になっているということがわかります。正確に説明するならば、Rubyでは、最終的に評価された式が実質返り値となるので、上記の場合はreturnがいらないのではあるのですが。

つまり、最終的に、その値を何らかの形で受け取ってあげられれば、それで越したことが無いのです。

FizzBuzzで利用できることを調べる

多くのプログラミング言語の場合、整数を0で割った場合エラーが置きます。例えば、Rubyの場合ですと、ZeroDivisionErrorというのが発生します。普通、割りきれるといった場合、余りを求めるオペレーターにおいて「0」になるものだということが出来ます。例えば、3の倍数ならばx % 3 == 0がtrueになるようなxというように考えられます。

とすると、基本的に「0」で割ってしまうときだけ、エラーが発生するということがわかります。そこで、ZeroDivisionErrorをキャッチすることで、人為的にエラーを起こすことが可能になると言えるでしょう。

実例

上のような、抽象的な説明をしつづけても仕方ないと思うので、先にサンプルコードを出しておきます。

class FizzBuzzError < StandardError;end

def base(q, str, nextfunction)
  lambda do |n|
    begin
      1 / (n % q)
      nextfunction.call(n)
    rescue ZeroDivisionError
      raise FizzBuzzError, str
   end
  end
end

@print_n  = lambda {|n| raise FizzBuzzError, n}
@fizz     = base(3,  "Fizz",     @print_n)
@buzz     = base(5,  "Buzz",     @fizz)
@fizzbuzz = base(15, "FizzBuzz", @buzz)

@start_point = @fizzbuzz

def run(n, start_function)
  start_function.call(n)
rescue FizzBuzzError => e
  return e.message
end


1.upto(100) {|n| puts run(n, @start_point) }

コードレビュー

先にコードレビューをしておきますと、実際のところは、class FizzBuzzみたいなものを作って、それを利用したほうがRubyらしいように感じました。今回はRubyでのクロージャの作り方もきちんと抑えたかったので、あえてクラス定義をすることなしに、直接のdef定義を行いました。

この実装でポイントになっているのは、次に呼び出す必要のあるlambdaを次々に連鎖させるような作りにしているというところでしょうか。これに関しては、連結リストなどのデータ構造を利用して実現するほうが、柔軟性が高いかなという気もします。

なにはともあれ、このように連鎖させることによって、途中でZeroDivisionErrorが発生すれば抜け出すことが可能になりますので、それを連鎖の元であるrunでキャッチしたのちに、そこで付随しているmessageを出力すれば、無事FizzBuzzの想定通りのコードが出てくるようになります。

蛇足: カスタムエラー定義について

個人的には、今回のように「エラーを使って、一番上位のrescueまで駆け上がる」という実装の場合、そのまま、ただのrescueを使ってしまうと、意図しない実装エラーもキャッチしてしまうという問題がでてきます。なので、意図されているエラーを別途用意して、それをキャッチするような仕組みにしたほうが良いかと思っています。

今回の場合ですと、意図的にエラーを起こしてrescueを捕まえるための専門のエラーですので、あえてFizzBuzzErrorというものを用意しました。(ちなみに、本当ならZeroDivisionErrorをモンキーパッチして、結果をmessegeに埋め込んで送り込むほうがよりよいかと思いますが、Ruby力がないので、あえてこういう形にしています)

まとめ

というわけで、今回はエラーを使ってFizzBuzzをやる方法について書いてみました。

ただ、最後に一点だけなのですが、基本的にはこのようにsuper returnとしてエラーを使う方法は、あまり良い実装設計ではないように感じます。というのは、自分の理解だと、エラーというのは、その現象が起きた場合に、処理に不都合が発生してしまうという意図で使うのが望ましいかと考えています。

とはいえ、一概に「処理に不都合が発生してしまう」という定義も難しく、例えばPythonのdjangoフレームワークだと、ORMのクエリを発行し、そのレコードが存在しない場合にDoesNotExistを発生させるということがありますし、またRuby on Railsでも、そのようにレコードが無い場合においてはエラーを発生させるメソッドが別途用意されていたりします。このように考えると、そもそもエラーでその関数から脱出する場合、実装の意図と結びついているとも言えるのかな、という気がしています。

いろいろと書いては見ましたが、このようにエラーを使って、あえてifでいいところを使わない練習というのは勉強になるので、各自でも何かで試してみるといいのかもしれません。