Ruby で 50 行ほどのルールエンジンを書いてみた。

by tanabe on March 10, 2009

drools の記法が DSL らしくて「へー、いいなー」と思ったりしたので、なんとなく書いてみた。バックトラックを考慮していない(最初の答えを見つけたら break する)うえに速度が速いわけでもないので記法の違う if 文と言われると返す言葉もない。

要は rule, when, then のブロックでルールを定義するというのをやってみたかったということで。

when と then はメソッド定義まではできるんだけど DSL 風なアクセスをうまいこと API として定義できなくて _when と _then で妥協した。残念。

class Rrools
  class Rule
    def initialize
      @rule = {}
    end

    def _when &condition
      @rule[:condition] = condition
    end
    alias on _when

    def _then &consequence
      @rule[:consequence] = consequence
    end
    alias run _then

    def pack context
      instance_eval &context
      @rule[:context] = self
      @rule
    end
  end

  def self.describe &block
    rools = self.new
    rools.instance_eval &block
    Proc.new {|&context| rools.valuate &context }
  end

  def initialize
    @rules = []
    @description_context = Proc.new {}
  end

  def rule &rule_block
    rule = Rule.new
    rule.instance_eval &rule_block
    @rules << rule.pack(@description_context)
  end

  def context &context # rule の生成時に都度評価される
    @description_context = context
  end

  def valuate &context
    @rules.each do |rule|
      rule[:context].instance_eval &context
      if rule[:condition].call
        break rule[:consequence].call
      end
    end
  end
end

使い方はこんなかんじ。

rule = {}
rule["入場料"] = Rrools.describe do
  rule do
    _when { @people > 4 }
    _then { 5000 * @people }
  end

  rule do 
    _when { @people <= 4 }
    _then { 6000 * @people }
  end
end

people = 3
puts rule["入場料"].call { @people = people }  # => 18000

people += 1
puts rule["入場料"].call { @people = people }  # => 24000

people += 1
puts rule["入場料"].call { @people = people }  # => 25000


# やってみる。
# http://amateras.sourceforge.jp/cgi-bin/fswiki/wiki.cgi/free?page=Drools

class User
  attr_accessor :age, :name
  def initialize name, age
    @age = age
    @name = name
  end
end

is_adult = Rrools.describe do
  rule do
     on { @user.age >= 20 }
    run { puts "#{@user.name} is an adult." }
  end

  rule do
     on { @user.age < 20 }
    run { puts "#{@user.name} isn't an adult." }
  end
end

is_adult.call { @user = User.new("Bob", 21) }
is_adult.call { @user = User.new("Amy", 17) }