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

fizzbuzzを解くDSLを書いてみる

More than 3 years have 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面白い❗

コード

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

globis
グロービスは 1992 年の創業以来、社会人を対象とした MBA、人材育成の領域で Ed-Tech サービスを提供し、現在は日本 No.1 の実績があります。これらの資産と、さらに IT や AI を活用することで、アジア No.1 を目指しています。
http://www.globis.co.jp/
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