Watch your self
Fri Feb 05 02:36:49 -0800 2010
Blocks and closures are probably the most powerful, and least understood part of the Ruby programming language, combined instance_eval, it can create some unintuitive bugs.
Background
In my Mail library for Ruby, I have made use of instance_eval to provide a domain specific language for email, this allows me to offer the following as valid Ruby code:
m = Mail.new do
to 'mikel@test.lindsaar.net'
from 'you@test.lindsaar.net'
subject 'This is a valid email'
body "When this block closes it will " +
"return an email message."
end
This bit of code is calling the #new method on the Mail module, which accepts a block. This which creates a new Mail::Message, passing the block of ruby code which in turn calls instance_eval on itself, passing the Ruby code that is inside the block of code that we wrote between the do and end keywords.
The newly created Mail::Message now runs that block on itself, calling the to(), from(), subject() and body() methods in turn, passing the strings we gave it.
Once done, the Mail module then calls deliver on the Mail::Message and the email is sent on its way.
The code that makes the above happen is inside the Mail library:
module Mail
class Message
def initialize
# ... initialization
if block_given?
instance_eval(&block)
end
end
end
def Mail.new(*args, &block)
Mail::Message.new(args, &block)
end
end
Gotcha
All of the above is pretty straight forward and it works pretty much as you would expect, but there is one use case that will trip you up.
Suppose you don’t want to pass in strings to your Mail object? Suppose you want to pass in instance variables instead? Something like this:
@text = "When this block closes it will " +
"return an email message."
@to = 'mikel@test.lindsaar.net'
@subject = "This is a valid email"
Mail.new do
to @to
from 'you@test.lindsaar.net'
subject @subject
body @text_body
end
This ruby code will run successfully, but it will not return what you think. This is because the instance variables you pass into the block are evaluated within the scope of the Mail::Message and inside the Mail::Message, text</tt>, <tt>to and @subject all evaluate to nil.
This is where you need to watch your self. Inside of the Mail.new do.. end block, self effectively becomes the Mail::Message, and the mail message has not defined the instance variables you are passing in.
Solutions
To get around this, you can do a number of things.
First, you can use methods instead of instance variables:
def body_text
"When this block closes it will " +
"return an email message."
end
def to_address
'mikel@test.lindsaar.net'
end
def default_subject
"This is a valid email"
end
Mail.new do
to to_address
from 'you@test.lindsaar.net'
subject default_subject
body body_text
end
This is obviously a lot more code, but in practice you usually have the to, subject and body text already being generated by methods inside your code, so it can work.
The other solution is a lot more simple, don’t use a block:
@text = "When this block closes it will " +
"return an email message."
@to = 'mikel@test.lindsaar.net'
@subject = "This is a valid email"
m = Mail.new
m.to @to
m.from 'you@test.lindsaar.net'
m.subject @subject
m.body @text
Which is arguably less pretty, but you are not going to get caught out.
blogLater
Mikel




Sat Feb 06 10:56:18 -0800 2010
This seems like a pretty big “gotcha”, what do you see as the benefit of this approach over something like passing self into the block rather than using instance_eval?
Mon Feb 22 09:23:44 -0800 2010
Thanks for this clarification. I was bitten by empty instance variables inside a block and am glad to have a (less-pretty but working) solution
Thu Apr 01 04:15:19 -0700 2010
Can’t one hack Mail#initialize to “get” all current instance variables, ‘magically’?
Tue Nov 08 19:46:59 -0800 2011
This seems like a pretty big “gotcha”, what do you see as the benefit of this approach over something like passing self into the block rather than using instance_eval?
Tue Jan 31 04:19:36 -0800 2012
The best person to give you medical advice about liver disease is your doctor. Best thing we can do is recommend perhaps a good doctor if you need a second or third opinion. casino