A Ruby rule engine

Published: 6:08 PM GMT+12, Saturday, 13 August 2005 under: technology
java  smalltalk  ruby  rules 

Yet another rule engine, first there was the java, smalltalk, and python versions, and after James Robertson mentioned I'd also done a Ruby version I thought I best actually do one...

I've been following alot of Ruby blog posts but as yet havn't actually written anything in it. I'd seen enough sample code on blogs to know the basic layout of the structure, so I fire up gedit and define a class:

class RuleSet
end

My, that was hard :) Now, theres two bits of functionality I know I want: 1) The ability to make an assertion against the rule and 2) The ability to run all defined assertions against a value. One of the strengths of Ruby is its support for blocks, and the basic design follows the Smalltalk version pretty much identically:

class RuleSet
  def initialize()
    @assertions = []
  end

  def assert(&assertion)
    @assertions.insert(0, assertion)
  end

  def for(value)
    @assertions.each {|assertion|
      if !assertion.call(value)
        return false
      end
    }
    return true		
  end
end

Again this is looking very simple, its not complex code, but the lack of static typing, blocks, and a collections API that takes advantage of it make for clean and consise code.

rule = RuleSet.new
rule.assert {|value| value == "mark"}

ruleSet = {:nameIsMark, rule}

Create a RuleSet, make some assertions, put it in a map keys by a symbol (I initially used a string, but I think a symbol is more appropriate here). Simple.

if ruleSet[:nameIsMark].for("mark")
	puts "rule passed"
end

Look ma - NO CASTING!

Comments (1)

This rule engine seems to be somewhat related to the kind of contracts I have implemented at http://ruby-contract.rubyforge.org/ -- perhaps you will find it interesting.

The Ruby code itself was pretty idiomatic.

I think I would slightly change the style, though:

class RuleSet
  def initialize(&block)
    @assertions = []
    instance_eval(&block) if block
  end

  def assert(&assertion)
    # You can also do Array#unshift if the original semantics are important
    @assertions << assertion 
  end

  def for(value)
    @assertions.all? do |assertion|
      assertion.call(value)
    end
  end
end

You might also want to provide the === method and instance_eval a supplied block in initialize:

class RuleSet
  alias :=== :for
end

positive_even_number = RuleSet.new do
  assert { |obj| obj.is_a?(Numeric) }
  assert { |obj| obj > 0 }
  assert { |obj| obj % 2 == 0 }
end

case gets.to_i
  when 42
    puts "Totally good choice."
  when positive_even_number then
    puts "Not too bad."
  else
    puts "What are you doing!?"
end

puts [1, 4, 16, 9, "foo", 1.5].grep(positive_even_number) # outputs 4 and 16

I wasn't able to comment on your weblog with the popular Mozilla Firefox browser. Can you please fix this?

left by Florian Groß . Sunday, 14 August 2005 8:00 AM
Add Comment