Ruby
メタプログラミング

fizzbuzzを解くDSLを書いてみる

More than 1 year has passed since last update.


fizzbuzzをなるべく小難しく解きたい

(前回のつづき)

みんな大好きなfizzbuzzです。

無駄に頑張ったfizzbuzzを目指して試行錯誤した結果、専用のDSLを作ることになりました。

・・・と、大きく出てみたけど、DSLについて正確に理解していません。

間違っていたらごめんなさい。


どんなものをつくりたいのか


  • 判定対象となる数字が自由に設定・変更できる

  • 表示メッセージや判定ロジックを自由に追加できる

  • 判定用の新規メソッドも自由に追加できるようにする

  • モジュールをインクルードするとその場所だけで使える(やみくもにmainを汚染しない)

実装のイメージで言うとこんな感じです。


module FizzBuzzTest
include FizzBuzzDSL

set_counter from: 10, to: 15 # どこからどこまでカウントするかを決める

rule_for :fizzbuzz do |n|; n % 15 == 0; end # ルールは上から優先して適用される
rule_for :fizz do |n|; n % 3 == 0; end
rule_for :buzz do |n|; n % 5 == 0; end

fizzbuzz #実行

end

# (ズラズラズラ・・と結果が並ぶ)


とりあえず書いてみる


module FizzBuzzDSL # 実行時にはこいつをインクルードしてもらう

def self.included(klass)
klass.extend FizzBuzzMethod
klass.class_eval do
@setups = []
@rules = []
@min = 1 #デフォルト最小値
@max = 20 #デフォルト最大値
end
end

end

インクルードされたら、インクルード元のモジュール(クラス)の特異メソッドをもろもろ定義する。('FizzBuzzMethod'内に定義しておく)

ついでに設定を保存しておくクラスインスタンス変数もセットしておく。

これによって、DSLは呼ばれたモジュール内だけで有効になる、はず。

次にDSLのコマンドを形づくるFizzBuzzMethodの中身を書く。


module FizzBuzzMethod

def set_counter (from: nil, to: nil) # 最小値、最大値を両方セットする

@min = from || @min # 最小値: 後から指定されたら上書き
@max = to || @max # 最大値
puts "warning: counter set from #{from} to #{to} (wrong order)" if @min > @max # 順序が逆なら一応警告を出しとく
end

def count_from(min) # スタート値だけ設定できる
@min = min
end

def count_upto(max) # 終了値だけ設定
@max = max
end

ルールは、Procを来た順番に保存していき、後で順番に呼び出します。


def rule_for(script='', &block)
return false unless block_given? # 基本おかしなものは静かにスルー(例外は出さない)
@rules << {script: script, condition: block}
end

def reset_rules # 設定されたルールを全リセットできるようにする
@rules = []
end

そして肝心の実行部分です。


def fizzbuzz(label: true, skip: false)

if skip==false # (オプション)が与えられた条件が全て偽のとき、スキップせず数字を出力する
noskip = {script: '', condition: proc.new{ true } }
@rules << noskip
end

@min.upto @max do |num| # ここから中身
prefix = label && "#{num}: " # (オプション) 読みやすいように行頭に数字ラベルをつける

@rules.each do |rule|
if rule[:condition].call (num) # 条件を判定
if rule[:script].empty?
puts "#{prefix}#{num}" # 文字列が指定されていない場合は現在の数字を出力
else
puts "#{prefix}#{rule[:script]}"
end
break # 一つでも条件に該当したら次の数字へ
end
end
end
end

(おまけ)

せっかくなので、判定用のオリジナルメソッドも追加できるようにしておく。

これでfizzbuzzと全く関係ないものも動くよ


def add_evaluator(meth, *arg, &block)
meth.to_sym if meth.is_a?(String)
return false unless meth.is_a?(Symbol)
return false unless block_given?

self.define_singleton_method meth, *arg, &block # 実質これだけ
end


動かしてみる

というわけで、つらつらとDSLを書いてみる


module FizzBuzzTest # モジュールの中で呼ぶ
require './FizzBuzzDSL'
include FizzBuzzDSL # 先程のをインクルード

count_from 10 #ひとまず10-20(デフォルト)でやってみる

rule_for :fizzbuzz do |n|; n % 15 == 0; end
rule_for :fizz do |n|; n % 3 == 0; end
rule_for :buzz do |n|; n % 5 == 0; end

fizzbuzz

end

ゴクリ。

10: buzz

11: 11
12: fizz
13: 13
14: 14
15: fizzbuzz
16: 16
17: 17
18: fizz
19: 19
20: buzz

おおー。できてる。ルビーすげー

完全数や素数の判定もできます。(ムダ機能)

module SpecialNumbers

require 'prime' # 外部ライブラリも使える
require './FizzBuzzDSL'
include FizzBuzzDSL # 最初にインクルードする

set_counter from: 5, to: 30

add_evaluator :perfect_num? do |num| # 完全数を判定するメソッド
return false if num == 1
num == sum_divisors(num, num-1) # 注:再帰的に計算しているため、桁を増やすとstack level too deepエラーになる(残念)
end

add_evaluator :sum_divisors do |num, i| # 計算補助用のメソッドも定義できる
raise ArgumentError if i<=0
return 1 if i==1
if (num % i).zero?
i + sum_divisors(num,i-1)
else
sum_divisors(num,i-1)
end
end

rule_for (:PERFECT) { |n| self.perfect_num?(n) } # 完全数
rule_for (:prime) { |n| n.prime? } # 素数
rule_for (:square) {|n| Math.sqrt(n).round ** 2 == n } # 平方数

fizzbuzz label: true, skip: true # 判定条件にかからなければ何も出力しない

end

これを実行すると・・・

5: prime

6: PERFECT
7: prime
9: square
11: prime
13: prime
16: square
17: prime
19: prime
23: prime
25: square
28: PERFECT
29: prime

こんなことが簡単にできるルビースゴー❗


補足

1) ブロックの優先順位

書き方を省略するとエラーになったりします。


rule_for :fizz do |n|; n % 3 == 0; end # OK
rule_for (:fizz) { |n| n % 3 == 0 } # OK
rule_for :fizz { |n| n % 3 == 0 } # エラー

# rule_for (:fizz {|x| x % 3 == 0})と解釈される

最後くらいな感じで書けるとシンプルなんだけど、うまいやり方があるのでしょうか?

2) メソッドの定義

普通にdefを使ってもできます。

(selfを外すと呼び出せないので注意)


def self.even_number? (num) # 偶数を判定する
num % 2 == 0
end

こっちのほうがシンプルですね。

「メソッドを生成するメソッドを作ってみたかった」だけでしたw


参考図書

メタプログラミングruby

めちゃくちゃ勉強になりました。ruby面白い❗


コード

ニーズは全くないと思うけど、実際のコードはこちらにおいておきます。