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
でいいところを使わない練習というのは勉強になるので、各自でも何かで試してみるといいのかもしれません。