Watch your self

5 February 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, @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

results matching ""

    No results matching ""