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.
Evaluating Stream Processing Technologies: A Comprehensive Guide for Enterprise Architects
Related Sponsored Content
Related Sponsor
Hazelcast modernizes applications with a unified real-time data platform to act instantly on data. Learn more.
klass = Class.new
klass.class_eval do
include ERB::Utildef encoded_hello
htnl_escape "Hello World"
end
endklass.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_helloklass.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
enddef multiply(arg)
"select #{arg}"
enddef two(arg=nil)
"2#{arg}"
enddef 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
enddef multiply(arg)
eval arg
enddef two(arg=nil)
"2#{arg}"
enddef 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 < DslContextbubble :than, :is, :list, :the, :to
def more(value)
'> ' + value.to_s
enddef method_missing(sym, *args)
@stakes = sym
eval "List.size_for(sym) #{args.first}"
enddef floor(value)
__position(value, :floor)
enddef brush(value)
__position(value, :brush)
enddef open
__action(:open)
enddef announce
__action(:announce)
end
def __action(to)
{ :action => to }
enddef __position(value, title)
value[:position] = title
value
enddef notify(value)
[@stakes, value]
endend
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
endContextOne.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
enddef self.bubble(*methods)
methods.each do |method|
define_method(method) { |args| args }
end
enddef self.polish_text(text)
rules = text.split("\n")
rules.collect do |rule|
rule.gsub!(/'.+'/,extract_stakes(rule))
rule << " end"
end
enddef self.extract_stakes(rule)
stakes = rule.scan(/'.+'/).first
stakes.delete!("'").gsub!(%q{$},'dollar').gsub!('-','dash').gsub!(' ','space')
enddef 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 < DslContextbubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush
def announce
@stakes
endalias open announce
def method_missing(sym, *args)
@stakes = sym
endend
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 < DslContextbubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open, :open
def announce; end
def open; enddef brush(value)
:brush
enddef floor(value)
:floor
enddef method_missing(sym, *args)
true
endend
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
enddef [](*args)
@text = "delete from "
@text += args.join ","
@tables = args
self
endattr_reader :tables
def where(&block)
@text += " where "
instance_eval &block
enddef exists(statement)
@text += "exists "
@text += statement
end
endclass Select
def self.[](*args)
self.new(*args)
enddef initialize(*columns)
@text = "select "
@text += columns.join ","
enddef from
@text += " from "
self
end
def [](*args)
@text += args.join ","
@tables = args
self
enddef tables
@tables
enddef where(&block)
@text += " where "
tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect
instance_eval &block
enddef method_missing(sym, *args)
super unless @tables.include? sym
klass = Class.new
klass.class_eval do
def initialize(table)
@table = table
enddef method_missing(sym, *args)
@table.to_s + "." + sym.to_s
end
end
klass.new(sym)
enddef 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.