BT

Evaluation Options in Ruby

Posted by Jay Fields on Feb 14, 2007 |

Introduction

One of my favorite features of Ruby is the ability to evaluate a string or a block of code. Ruby provides a few different types of evaluation options; however, the evals I use most often are: eval, instance_eval, and class_eval

Module.class_eval

The class_eval (and the aliased module_eval) method of Module allows you to evaluate a string or block in the context of a class or module.

Common usages for class_eval include adding methods to a class and including other modules in a class.


klass = Class.new
klass.class_eval do
include ERB::Util

def encoded_hello
htnl_escape "Hello World"
end
end

klass.new.encoded_hello #=> <b>Hello World</b>

The above behavior can be achieved without using class_eval; however, a fair amount of readability must be traded.

klass = Class.new
klass.send :include, ERB::Util
klass.send :define_method, :encoded_hello do
html_escape "Hello World"
end
klass.send :public, :encoded_hello

klass.new.encoded_hello #=> <b>Hello World</b>

Object.instance_eval

The instance_eval method of Object allows you to evaluate a string or block in the context of an instance of a class. This powerful concept allows you to create a block of code in any context and evaulate it later in the context of an individual instance. In order to set the context, the variable self is set to the instance while the code is executing, giving the code access to the instance's variables.

class Navigator
def initialize
@page_index = 0
end
def next
@page_index += 1
end
end
navigator = Navigator.new
navigator.next
navigator.next
navigator.instance_eval "@page_index" #=> 2
navigator.instance_eval { @page_index } #=> 2

Similar to the class_eval example, the value of an instance variable can be gotten in other ways; however, using instance_eval is a very straightforward way to accomplish the above task.

Kernel.eval

The eval method of Kernel allows you to evaluate a string in the current context. The eval method also allows you to optionally specify a binding. If a binding is given, the evaluation will be performed in the context of the binding.

hello = "hello world"
puts eval("hello") #=> "hello world"

proc = lambda { hello = "goodbye world"; binding }
eval("hello", proc.call) #=> "goodbye world"

Expanding the context of eval

My first usage of eval was when I created the attr_init class method. I found that I was often repeating the following pattern in the code.

def some_attribute
@some_attribute || = SomeClass.new
end

I decided to create a class method that would encapsulate the above behavior.

class << Object
def attr_init(name, klass)
define_method(name) { eval "@#{name} ||= #{klass}.new" }
end
end

I remember feeling like the call to eval was ugly, but at the time I couldn't think of another way to complete the task. I posted the code on my blog for all to criticize, and they quickly did. It was not obvious at first to me, but the following solution is far superior since it only requires one call to eval instead of a new eval with each call to the defined method.

class << Object
def attr_init(name, klass)
eval "define_method(name) { @#{name} ||= #{klass}.new }"
end
end

This optimization is interesting in that it requires increasing what is evaluated for better performance. From that point forward I used eval when necessary; however, I was careful to keep an eye toward using it as efficiently as possible.

Using instance_eval in multiple contexts

It can often be valuable to evaluate blocks (or code as strings) in various contexts. This is a very common technique used when designing a Domain Specific Langauges (DSL). In fact, one key to using a DSL is the ability to evaluate in various contexts. Take this code for example:

class SqlGenerator
class << self
def evaluate(&script)
self.new.instance_eval(&script)
end
end

def multiply(arg)
"select #{arg}"
end

def two(arg=nil)
"2#{arg}"
end

def times(arg)
" * #{arg}"
end
end

The above code allows you to generate a SQL statement by calling the SqlGenerator.evaluate method with a block:

SqlGenerator.evaluate { multiply two times two }
=> "select 2 * 2"

However, you could also execute the same code in the context of a calculator class to receive a result:

class Calculator
class << self
def evaluate(&script)
self.new.instance_eval(&script)
end
end

def multiply(arg)
eval arg
end

def two(arg=nil)
"2#{arg}"
end

def times(arg)
" * #{arg}"
end
end

Which executes as:

Calculator.evaluate { multiply two times two }
=> 4

The above code demonstrates how to use instance_eval to specify the scope in which a block executes. As I previously documented, the instance_eval method evaluates either a string or block within the context of the receiver. The receivers in my examples were a new instance of SqlGenerator and a new instance of Calculator. Also, be sure to note that I call self.new.instance_eval. If I don't call the new method of self, the block will be evaluated by the class and not an instance of the class.

The above code also shows the first few steps towards defining a DSL. Creating an DSL is challenging, but it does provide many advantages. One advantage to expressing your business rules in a DSL is the ability to execute them in various contexts. As the above example shows, by executing the DSL in various contexts you can generate multiple behaviors from the same business rule. When the rule changes over time, all parts of the system that reference the rule will also be changed. The key to achieving this success is taking advantage of Ruby's evaluation methods.

The Casino Poker Table example

Using the evaluation options it is easy to execute in various contexts. For example, assume you work for a casino and you have been tasked with designing a system that will notify the poker room employees when a new poker table needs to be opened or when you are looking to open a new poker table. The rules for opening a poker table vary based on the stakes of the table and the length of the waiting list. For example, you need more people waiting for a no-limit game because people are more likely to lose all their money on one hand and you don't want to open the table and need to close it shortly after because there aren't enough players. The rules would be expressed in your DSL like this:
if the '$5-$10 Limit' list is more than 12 then notify the floor to open
if the $1-$2 No Limit' list is more than 15 then notify the floor to open
if the '$5-$10 Limit' list is more than 8 then notify the brush to announce
if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce

The first context in which I will execute the DSL is the context that notifies the employees.

class ContextOne < DslContext

bubble :than, :is, :list, :the, :to

def more(value)
'> ' + value.to_s
end

def method_missing(sym, *args)
@stakes = sym
eval "List.size_for(sym) #{args.first}"
end

def floor(value)
__position(value, :floor)
end

def brush(value)
__position(value, :brush)
end

def open
__action(:open)
end

def announce
__action(:announce)
end
def __action(to)
{ :action => to }
end

def __position(value, title)
value[:position] = title
value
end

def notify(value)
[@stakes, value]
end

end

ContextOne is executed by the following code.

script = <<-eos   if the '$5-$10 Limit' list is more than 12 then notify the floor to open
if the '$1-$2 No Limit' list is more than 15 then notify the floor to open
if the '$5-$10 Limit' list is more than 8 then notify the brush to announce
if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce eos

class Broadcast
def self.notify(stakes, options)
puts DslContext.sym_to_stakes(stakes)
options.each_pair do |name, value|
puts " #{name} #{value}"
end
end
end

ContextOne.execute(script) do |notification|
Broadcast.notify(*notification)
end

ContextOne inherits from DslContext. The definition of DslContext can be found below.

class DslContext
def self.execute(text)
rules = polish_text(text)
rules.each do |rule|
result = self.new.instance_eval(rule)
yield result if block_given?
end
end

def self.bubble(*methods)
methods.each do |method|
define_method(method) { |args| args }
end
end

def self.polish_text(text)
rules = text.split("\n")
rules.collect do |rule|
rule.gsub!(/'.+'/,extract_stakes(rule))
rule << " end"
end
end

def self.extract_stakes(rule)
stakes = rule.scan(/'.+'/).first
stakes.delete!("'").gsub!(%q{$},'dollar').gsub!('-','dash').gsub!(' ','space')
end

def self.sym_to_stakes(sym)
sym.to_s.gsub!('dollar',%q{$}).gsub!('dash','-').gsub!('space',' ')
end
end

The method_missing method of ContextOne also expects the following List class to be defined.

class List
def self.size_for(stakes)
20
end
end

ContextOne uses the DSL to check the List for the size per stakes and sends notifications when necessary. This is of course sample code and my List object is just a stub to verify that everything works correctly. The important thing to note is that the execute method is delegating to instance_eval to evaluate the code in the context of ContextOne.

Based on this same script you could execute a second context that returns a list of the different games that are currently being spread.

class ContextTwo < DslContext

bubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush

def announce
@stakes
end

alias open announce

def method_missing(sym, *args)
@stakes = sym
end

end

As you can see, adding additional contexts is very easy. Since the execute method of DslContext uses the instance_eval method the above code can be executed with the code below.

ContextTwo.execute(script) do |stakes|
puts ContextTwo.sym_to_stakes(stakes)
end

To drive the example home we will create another example to display all positions that are set up to receive notices.

class ContextThree < DslContext

bubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open, :open

def announce; end
def open; end

def brush(value)
:brush
end

def floor(value)
:floor
end

def method_missing(sym, *args)
true
end

end

Again, the context inherits from DslContext, which uses instance_eval, so the only code necessary to execute is the following code.

ContextThree.execute(script) do |positions|
puts positions
end

Being able to evaluate a DSL script in multiple contexts begins to blur the line between code and data. The script 'code' can also be evaluated to do things such as generate reports (i.e. A report of which employees are contacted by the system). The script could also be evaluated in a context that will show how long before a table will be opened (i.e. the rule states that 15 are needed, the system knows 10 are on the list so it displays the message '5 more people needed before the game can start'). Using instance_eval allows us to evaluate the same code in any way necessary for our systems.

Eval is magic also

The above examples show how to evaluate blocks in different scopes by using instance_eval. But, eval also allows you to evaluate in more than one context. Next I'll show how you can evaluate a string of ruby code in the scope of a block.

At the beginning we started with a simple example, but lets revist a simple case where we use eval with the binding of a block. For this example we will need a class that can create a block for us.

class ProcFactory
def create
Proc.new {}
end
end

In the example, the ProcFactory class has a method, create, that simply creates and returns a proc. Despite the fact that this doesn't look very interesting, it actually provides you with the ability to evaluate any string of ruby code in the scope of the proc. This gives you the power to evaluate ruby code in the context of an object without directly having to reference it.

proc = ProcFactory.new.create
eval "self.class", proc.binding #=> ProcFactory

When would you ever actually use such a thing? I recently used it while developing a DSL to represent SQL. I started with the following syntax.

Select[:column1, :column2].from[:table1, :table2].where do
equal table1.id, table2.table1_id
end

When the above code is evaluated the [] instance method (following from) saves each table name in an array. Then, when the where method is executed it yields the block given to where. When the block is yielded the method_missing method is called twice, once with :table1 and then with :table2. When method_missing is called we check that table name array (which was created in the previously mentioned [] method) includes the symbol argument (:table1 and :table2) to verify it is a valid table name. If the table name is included in the table name array we return an object that knows how to react to column names. However, if the table name is invalid we call super and raise NameError.

All of this works perfectly, until you start using sub-queries. For example, the following code will not work with the current implementation.

Delete.from[:table1].where do
exists(Select[:column2].from[:table2].where do
equal table1.column1, table2.column2
end)
end

Unfortunately, this needed to work, and using eval and specifying a binding was how we made it work. The trick is getting the table names array from the outer block into the inner block without explicitly passing it in somehow (which would have made the DSL ugly).

To solve this problem, within the where method of the Select class we used the binding of the block to get the tables collection from the Delete instance. This is possible because the where method of the Delete instance is the context aka the binding of the block being passed to the where method of the select instance. The binding (or context) is the scope in which the block is created. The following code is the full implementation of the where method.

def where(&block)
@text += " where "
tables.concat(eval("respond_to?(:tables) ? tables : []",
block.binding)).inspect
instance_eval &block
end

Let's break the eval statement apart and see what it's doing. The first thing it's going to do is

eval "respond_to?(:tables) ? tables : []", block.binding

Which simply means "eval that statement within the scope of the block". In this case the scope of that block is:

Delete.from[:table1].where do .. end

This scope is a Delete instance, which does have a tables method that exposes the tables array (tables #=> [:table1]). Therefore, the statement will evaluate and return the tables array, and the rest of the statement could be pictured as this:

tables.concat([:table1])

This is simply going to concatenate all the table names into the tables array available in the inner block. After this one line change, the statement now produces expected results.

delete from table1 where exists (select column2 from table2 where table1.column1 = table2.column2)

The following code can be used to generate the above result and can be used as a reference for using a binding with eval.

class Delete
def self.from
Delete.new
end

def [](*args)
@text = "delete from "
@text += args.join ","
@tables = args
self
end

attr_reader :tables

def where(&block)
@text += " where "
instance_eval &block
end

def exists(statement)
@text += "exists "
@text += statement
end
end

class Select
def self.[](*args)
self.new(*args)
end

def initialize(*columns)
@text = "select "
@text += columns.join ","
end

def from
@text += " from "
self
end
def [](*args)
@text += args.join ","
@tables = args
self
end

def tables
@tables
end

def where(&block)
@text += " where "
tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect
instance_eval &block
end

def method_missing(sym, *args)
super unless @tables.include? sym
klass = Class.new
klass.class_eval do
def initialize(table)
@table = table
end

def method_missing(sym, *args)
@table.to_s + "." + sym.to_s
end
end
klass.new(sym)
end

def equal(*args)
@text += args.join "="
end
end

Conclusion

As you can see the evaluation methods of Ruby provide very good options for creating concise, readable code. The evaluation methods also provide the ability to easily create very powerful tools such as Domain Specific Languages.

About the author

Jay Fields is a software developer at ThoughtWorks. He is a early adopter who is consistantly looking for new exciting technologies. His most recent work has been in the Domain Specific Language space where he delivered applications that empowered subject matter experts to write the business rules of the applications.

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

error by haoxiang zhang

Select[:column1, :column2].from[:table1, :table2].where do
equal table1.id, table2.table1_id
end

it is should be use {} instend of do ... end

because of do...end will be send to Select method , not where.

Re: error by haoxiang zhang

sorry, it is my fault.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT