Accessing Instance Variables in Squeel (or, Fun with Bindings and instance_eval)
CommentsIn the spirit of the recent post on the ` method, I thought I might write a little bit about another interesting bit in the Squeel DSL. Again, less for the benefit of the Squeel user, and more from the “hey, this is a useful trick to have up your sleeve as a Ruby programmer” angle. If a few less people ask me how to access instance variables or methods from a Squeel DSL block, that’s just a bonus.
The Problem
So, before we get started, a reminder: The Squeel DSL is instance_evaled. This means that throughout your DSL block, self an instance of Squeel::DSL, not whatever it was outside your block. Your instance variables and instance methods will not work like you think they might. That is, you might expect to be able to write something like this:
This does not work (so don’t you go trying it)
class ArticlesController < ApplicationController
def show
@article = Article.where{id == params[:id]}.first
end
end
OK, you would probably just write Article.find(params[:id]), but let’s pretend you wouldn’t. Anyway, this example won’t work (as you may have read somewhere recently, like, a few lines up). Outside your DSL block, params is an instance method available to your ArticlesController instance. Inside the DSL block, it hits method_missing, generating a Stub, and things go horribly wrong from there. You will end up with something like the following, in fact:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: params.id: SELECT "articles".* FROM "articles" WHERE "articles"."id" = []("params"."id") LIMIT 1
Yeah. Pretty messed up, right? Right. So, one workaround (as outlined in the wiki) is to assign the values to a local variable. Those will still be available in an instance_evaled block. Not very practical. though. Squeel would really suck if there weren’t some way to easily access your class’s instance variables and methods. Thankfully, there is.
my{hero}
The assignment above can be rewritten like this, and work as expected:
This works (so you can totally try it)
@article = Article.where{id == my{params[:id]}}.first
What’s this my{} stuff all about? Well, it effectively breaks out of the Squeel DSL and back into the context that exists outside of the instance_eval, where things like params and @shaved_llama behave as expected.
By the way, if you’re here for the Squeel stuff, you can stop reading now. Thus endeth the lesson.
Still here? Good. Moving on.
That sounds like a pretty handy thing to know. How does that work?
Pretty much every class in Ruby (aside from 1.9’s BasicObject) inherits the Kernel#binding method. This method allows you to do a really neat, and probably questionable, thing. It returns an object of class Binding. What’s a binding? To quote the docs:
Objects of class Binding encapsulate the execution context at some particular place in the code and retain this context for future use. The variables, methods, value of self, and possibly an iterator block that can be accessed in this context are all retained.
This is seriously neat stuff. So, how do we actually execute code inside the my{} block against the previous context? Well, bindings are designed to be used with Kernel#eval (with the Binding as the second parameter), or by calling Binding#eval with the code to eval as a parameter. Unfortunately, both of these methods require a string of Ruby code, and won’t accept a block, so we still have some work to do.
How Squeel does it
So, in Squeel’s case, when you write Article.where{[…]}, what happens behind the scenes is that your block is sent to Squeel::DSL.eval and the result of this block is sent to the default ActiveRecord where method. Here’s a look at DSL.eval:
lib/squeel/dsl.rb
def self.eval(█)
if block.arity > 0
yield self.new(block.binding)
else
self.new(block.binding).instance_eval(█)
end
end
Ignore the first part – that’s for another instance method/variable workaround, covered in the wiki, in which we avoid using instance_eval and instead yield the DSL instance. We’re going to focus on the second one, which uses instance_eval. We pass the block’s binding to the Squeel::DSL initializer, which makes use of Binding#eval to capture the caller:
lib/squeel/dsl.rb
def initialize(caller_binding)
@caller = caller_binding.eval 'self'
end
Since we can only eval a string, we eval ‘self’ to get the object that originally called us. It almost feels like cheating. From that point, you can probably see where we’re going with this. Here’s the implementation of Squeel::DSL#my, complete with comments:
lib/squeel/dsl.rb
# If you really need to get at an instance variable or method inside
# a DSL block, this method will let you do it. It passes a block back
# to the DSL's caller for instance_eval.
#
# It's also pretty evil, so I hope you enjoy using it while I'm burning in
# programmer hell.
#
# @param █ A block to instance_eval against the DSL's caller.
# @return The results of evaluating the block in the instance of the DSL's caller.
def my(█)
@caller.instance_eval █
end
With the object that originally called the DSL available, another instance_eval call flips self back to its previous value, giving the appearance of “escaping” the instance_eval, like magic.
Why “my”?
I came to Ruby from Perl. In Perl, my is used to declare variables to be local within a given scope. I always liked Perl’s “my,” as it is both concise and expressive: “This is my variable. Don’t even think about touching it!”
In Squeel, though my performs a different function, I find it similarly expressive: “Give me my params and my @shaved_llama.”
I hope you’ve found this post useful! If you’d like to read more about bindings, you should really check out this post by Jim Weirich. All the way back in 2003, Jim was already writing about these nifty tricks, proving that he is, indeed, the man.
comments powered by Disqus