Discovering self-evaluating objects. (pt. 2 of 2)

by Rabbit

Notice: There’s quite a bit of code covered in this post. You can follow along with most of it using IRB. However, to run the final example, you’ll be better off downloading this complete Rails application. Follow the instructions in doc/readme.

Notice: This is part two of a two-part post. Read the first post if you haven’t already.

All right! We’re taking the experience we gained from our last endeavor, and we’re putting it on rails.

First off, let’s think about our goal — what are we trying to accomplish? How does it flow? What does it look like? In this case it’s pretty simple. It should probably look something like this:

1
product.evaluate('cost')

That’s concise, and it looks good. But it’s not the whole story. So let’s think some more.

We know we have products, as shown above. We have rules, presumably shown above as the argument to the evaluate method. What about variables? I don’t see those in the code above.

Before we go any further, let’s hash out a quick drawing to show our understanding of our objects and their relationships. … Done!

diagram

Pretty basic right now, but let’s talk about it.

Rules have many products have many variables. Interesting. When you say it like that, it almost doesn’t make sense. A rule has many products? What would that look like code-wise?

1
2
3
4
5
class Rule
 
  has_many :products
 
end

Hmm… looks perverted, (a rule has products?) but let’s keep going with some pseudo code.

1
2
product = rule.products.create(:name => 'Cheese')
product.variables.create(:name => 'cost', :value => 2)

I dig the second part:

1
product.variables.create(:name => 'cost', :value => 2)

So let’s keep that in mind as we progress.

But what about rule.products? Our first line of code tells us that products should be able to evaluate rules, so there’s gotta be some connection between the two. What do we know least about? I’d probably say the rule objects themselves right now, so let’s take a stab at designing one.

What are rules? Well, let’s look at a real-life example.

1
(x + y) * z

What do you see? … You could say you see a simple math formula. Perhaps, if you recall David West’s explanation of self-evaluating objects, you see an array of operators and variables. Before I tell you what I see, I’d like to say that what I see is based off my previous experience handling self-evaluating objects. That said, I see a string.

That’s it. A simple string.

1
"(x + y) * z"

It’s so simple it’s obvious. And obvious things are often the most difficult thing to see. Now that we know what a rule is, let’s try and define it using object parlance. What can a rule do? What’s it comprised of? What are its responsibilities? Who must it collaborate with to get its job done?

The first thing I see is a definition. Every rule has a definition. I suppose also, that a rule could have a name; it would certainly make distinguishing one rule from another easier.

So now we have named rules with definitions. Let’s see some potential code.

1
2
rule = Rule.new(:name => 'Product cost')
rule.definition = '(cost * tax) + cost'

Looks pretty clean. What next? Oh, that’s right, the obvious: a rule can evaluate itself. Ah, there’s the meat. Now we have a new, non AR-inherited responsibility: evaluation. Let’s take a look..

1
2
3
4
5
6
7
class Rule < ActiveRecord::Base
 
  def evaluate
    eval(definition)
  end
 
end

That’d be nice if that’s all we had to do. Unfortunately it’s not. Executing that as is will raise an error. If we think back to our earlier work, we did simple string replacement to get the proper values into our definitions.

1
2
3
def evaluate
  definition.gsub('uh oh!', "we're missing something")
end

Things just got interesting. Our rule doesn’t have access to the variables needed for substitution in its definition. Hmm… let’s stop and think for a moment.

Rules, products and variables. Products have variables for use in rules. A rule’s definition must have its variables substituted out for actual values. What does that look like?

1
2
rule = Rule.new(:name => 'Product cost', :definition => '(cost * tax) + cost')
rule.evaluate(product)

Hmm… that could work. A rule requires a product be passed to it to gather the contents of its variables.

Let’s try that.

1
2
3
4
5
6
7
8
9
10
11
class Rule < ActiveRecord::Base
 
  def evaluate(product)
    product.variables.each do |variable|
      definition.gsub!(variable.name, variable.value)
    end
 
    eval(definition)
  end
 
end

Not bad. A rule can evaluate itself in the context of a given product’s variables. This will work provide the product passed has all the variables required of the rule, and vice versa.

Of course, this model doesn’t look like our original diagram, and that’s okay. Things rarely go according to plan. Let’s review what we’ve done so far.

Rule objects stand alone. They are named mathematical definitions that evaluate in the context of a given product. (In reality, ‘product’ could be any object that responds to a variables method.)

So our end result is now:

1
rule.evaluate(product)

That’s the opposite of what I originally proposed (product.evaluate(rule)), and I’ll be honest; it feels weird. On the bright side, this solution is much simpler than my original Rails version. However, I still like the feel of:

1
product.evaluate(rule)

But hey, whatever works, right? There’s just one more thing… rules within rules. Ah, interesting. What would that look like?

Well, actually, it shouldn’t look any different than what we have now:

1
rule.evaluate(product)

The above should still give us a single number. So it’s not the end-result (API) code that will be changing, it’s our internal code that will change. But how? Let’s take a look at our definition.

1
(cost * tax) + cost

The part in parentheses represents the tax of our product. So we’re really saying:

1
tax + cost

But now we’ve run into a problem. Is tax a rule or a variable? Currently there’s no way to know the difference. How can we solve that? We need to be able to differentiate between rules and variables. Or at least, I think we should.

We could not, and simply perform blind string replacement, replacing everything in our definition with every match in our variables method. The result is a definition where the only non-numeric or operator characters must be rules. But that sounds pretty flimsy. I can easily see there being a tax variable and a tax rule. So let’s make clear the difference between rules and variables.

1
r:tax + v:cost

Heh, that works. It changes our replacement around a bit, but not too much.

1
2
3
4
5
6
7
def evaluate(product)
  product.variables.each do |variable|
    definition.gsub!('v:' + variable.name, variable.value)
  end
 
  eval(definition)
end

Okay, so we can still evaluate our variables. But what about other rules? What do we do when we encounter another rule? Let’s try scanning the string for instances of r:xxx, do a find for that rule name, and then call evaluate on it…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  def evaluate(product)
    # Operate on a copy of the definition so the original is open to variable
    # changes.
    evaluated_definition = definition.dup
 
    # Recursively evaluate any rules in the definition.
    evaluated_definition.scan(/r:\w+/) do |match|
      evaluated_definition.gsub!(match, Rule.find_by_name(match[2, match.length]).evaluate(product).to_s)
    end
 
    # Replace variables in the definition with values provided by product.
    product.variables.each do |variable|
      evaluated_definition.gsub!('v:' + variable.name, variable.value.to_s)
    end
 
    # Evaluate the fully-substituted string.
    eval(evaluated_definition)
  end

Let’s run this! (You’ll need to download the complete Rails app and follow the directions in doc/readme.)

1
2
3
4
5
6
7
8
9
10
11
12
rule = Rule.new(:name => 'Product cost')
rule.definition = 'r:tax + v:cost'
rule2 = Rule.new(:name => 'tax')
rule2.definition = '(v:cost * v:tax)'
rule.save
rule2.save
 
product = Product.new(:name => 'Cheese')
product.variables.create(:name => 'cost', :value => 10)
product.variables.create(:name => 'tax', :value => 0.05)
 
rule.evaluate(product) # => 10.5

Scha-weet! But wait! Do I see a recursive function in there? I sure do! Let’s deconstruct it.

We have a rule. We scan its definition for strings that look like r:xxx. Once found, we perform a global string replacement on it, using r:xxx as the pattern match, with the substitute string being the return value of calling evaluate on the rule referenced by r:xxx.

Was that confusing? Don’t worry if it was. Recursion can be pretty difficult to wrap your head around, especially if you didn’t write it. If you’re determined to understand, I suggest reading it several times, line by line, very slowly. It may also help to add some puts messages in there and run it a few times in IRB. That way you can “watch” the recursion magic.

Anywho, just know that it works, and will go as deep as you need it to.

So there we have it! A fully Rails-based self-evaluating monster that will slay accountants everywhere! Okay not really, but it’s pretty cool nonetheless, right?

As excited as you may be, don’t lose yourself. It still only works in console. If you choose to use a solution like this you’ll still need to build an interface for people to use it. There are some other small issues, too. Rule names, for instance. The way the evaluate method is written, you can’t have multiple rules with the same name. Well, you could, but you’d only ever be able to access the first one. There’s also the rigidity of relationships. Product specifically defines its relationship to variables, and there’s still the (I consider) perversion of calling rule.evaluate(product). I’d love to see product.evaluate(rule).

That, however, is an exercise I leave to you. ;)

Oh, before I go, here’s the complete code listing:

rule.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Rule < ActiveRecord::Base
 
  # Answers with a BigDecimal after recursively solving rule definitions,
  # performing variable replacement and finally evaluating a flat formula.
  #
  def evaluate(product)
    # Operate on a copy of the definition so the original is open to variable
    # changes.
    evaluated_definition = definition.dup
 
    # Recursively evaluate any rules in the definition.
    evaluated_definition.scan(/r:\w+/) do |match|
      evaluated_definition.gsub!(match, Rule.find_by_name(match[2, match.length]).evaluate(product).to_s)
    end
 
    # Replace variables in the definition with values provided by product.
    product.variables.each do |variable|
      evaluated_definition.gsub!('v:' + variable.name, variable.value.to_s)
    end
 
    # Evaluate the fully-substituted string.
    eval(evaluated_definition)
  end
 
end

product.rb

1
2
3
4
5
class Product < ActiveRecord::Base
 
  has_many :variables, :dependent => :destroy
 
end

variable.rb

1
2
3
4
5
class Variable < ActiveRecord::Base
 
  belongs_to :product
 
end

schema.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ActiveRecord::Schema.define(:version => 3) do
 
  create_table "products", :force => true do |t|
    t.column "name", :string
  end
 
  create_table "rules", :force => true do |t|
    t.column "name",       :string
    t.column "definition", :string
  end
 
  create_table "variables", :force => true do |t|
    t.column "product_id", :integer
    t.column "name",       :string
    t.column "value",      :decimal, :precision => 8, :scale => 2
  end
 
end