Although Instance_eval can be a great tool for creating clean DSLs it does, unfortunately, suffer from a few shortcomings. These shortcomings are caused by the ‘change in self’ of the block and may, in some circumstances, lead to surprising behaviour.
_why detailed a lot of these shortcomings in his blog post and proposed a wonderful solution: Don’t Change Self.
_why’s idea was to add the functionality that the instance_eval was to provide directly to the binding of the block itself. Using this technique we get the best of both worlds: we get the clean interface for our DSL and we eliminate the surprising and annoying behaviour of instance_eval in relation to local instance vars and methods.
How did _why do it?
_why first created a little C extension called ‘Mixico’ which makes it possible to ‘unmix’ modules from inheritance chains.His replacement for instance_eval, called mix_eval, works as follows:
- Get the binding of the block.
- Mix the Module containing the functionality we need into the binding.
- Yield to the block
- Unmix the Module from the binding (Using Mixico).
The above does work well and is a great and original idea but there are still a few problems with it.
The biggest problem (in my opinion) is that you must encapsulate the functionality you need in a Module; this results in the following syntax when invoking mix_eval:
mix_eval(my_module) { .../* the block */ }
#compare this to instance_eval
instance_eval {... /* the block */ }
This limitation is due to the fact that Mixico only works with Modules; it is not really possible (notwithstanding the recent rb_mod_mixin_object patch) to mix in Objects or Classes into the block binding.
Another problem is that mix_eval is not thread-safe.
A thread-safe mix_eval
One solution to the thread-safety issue was discovered by coderrr (http://coderrr.wordpress.com/2008/11/14/making-mixico-thread-safe/). His solution was to duplicate the binding of the block and perform the mix_eval on the duplicate.
However a run-of-the-mill duplicate is not a good solution if the user needs to modify the binding itself. The ‘duplicate’ was instead made to be (effectively) an IClass of the binding.
That is its m_tbl and iv_tbl pointers were set to the method table and ivar table of the original binding. coderrr’s solution worked as follows:
- Get the binding of the block
- Make an IClass from the binding
- Mix the Module containing the functionality we need into the IClass.
- Instance_Eval the block in the context of the IClass
- Unmix the Module from the IClass
- Reset the iv_tbl and m_tbl pointers of the IClass (to prevent a GC crash)
As far as I can tell coderrr’s solution is thread-safe (and very clever too IMO).
Dup_eval
Dup_eval is heavily based on coderrr’s work but with the realization that since we’re dealing with a duplicate we no longer need the machinery of Mixico.
That is, since the IClass is a duplicate of the actual binding we can simply discard it once we’ve finished and let it be GC’d (no need to unmix any Modules).
Another feature of Dup_eval is that it is not limited to mixing in Modules. It makes use of another small extension called Object2module that is capable of mixing in Objects and Classes.
As a result Dup_eval provides two methods: a straight forward dup_eval() that implicitly mixes in self and a dup_eval_with() method that lets you specify any number of Classes/Objects/Modules to mix into the binding. Note that the dup_eval() syntax is identical to instance_eval:
#dup_eval() syntax (implictly mixes self into the block binding)
my_object.dup_eval { /* ... the block... */ }
#dup_eval_with() syntax (does not implictly mix in self)
my_object.dup_eval_with(mod1, obj1, class1) { /* ...the block... */ }
Here is how Dup_eval works:
- Get the binding of the block
- Make an IClass from the binding
- Mix in the Object(s) (or Class(es) or Module(s)) into the IClass (Using Object2module)
- Instance_Eval the block in the context of the IClass
- Reset the iv_tbl and m_tbl pointers of the IClass (to prevent a GC crash)
- Discard the IClass
Summary
Dup_eval is just another attempt at expanding the power and flexibility of Ruby and is provided as-is in the hope it will aid or interest other Ruby developers.
For more information on possible applications of a program like dup_eval check out _why’s blog entry on mix_eval.
Also be sure to check out coderrr’s blog.
Downloads
Please be aware that dup_eval is a new project and possibly still has warts; use at own risk!
Install the gem: sudo gem install dup_eval
git project: http://github.com/banister/dup_eval/tree/master