Blockenspiel¶ ↑
Blockenspiel is a helper library designed to make it easy to implement DSL blocks. It is designed to be comprehensive and robust, supporting most common usage patterns, and working correctly in the presence of nested blocks and multithreading.
This is an introduction to DSL blocks and the features of Blockenspiel.
What's a DSL block?¶ ↑
A DSL block is an API pattern in which a method call takes a block that can provide further configuration for the call. A classic example is the Rails route definition:
ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end
Some libraries go one step further and eliminate the need for a block parameter. RSpec is a well-known example:
describe Stack do before(:each) do @stack = Stack.new end describe "(empty)" do it { @stack.should be_empty } it "should complain when sent #peek" do lambda { @stack.peek }.should raise_error(StackUnderflowError) end end end
In both cases, the caller provides descriptive information in the block, using a domain-specific language. The second form, which eliminates the block parameter, often appears cleaner; however it is also sometimes less clear what is actually going on.
How does one implement such a beast?¶ ↑
Implementing the first form is fairly straightforward. You would create a
class defining the methods (such as connect
in our Rails
routing example above) that should be available within the block. When, for
example, the draw
method is called with a block, you
instantiate the class and yield it to the block.
The second form is perhaps more mystifying. Somehow you would need to make
the DSL methods available on the “self” object inside the block. There are
several plausible ways to do this, such as using
instance_eval
. However, there are many subtle pitfalls in such
techniques, and quite a bit of discussion has taken place in the Ruby
community regarding how–or whether–to safely implement such a syntax.
I have included a critical survey of the discussion in the document ImplementingDSLblocks.rdoc for the curious. Blockenspiel takes what I consider the best of the solutions and implements them in a comprehensive way, shielding you from the complexity of the Ruby metaprogramming while offering a simple way to implement both forms of DSL blocks.
So what is Blockenspiel?¶ ↑
Blockenspiel operates on the following observations:
-
Implementing a DSL block that takes a parameter is straightforward.
-
Safely implementing a DSL block that doesn't take a parameter is tricky.
With that in mind, Blockenspiel provides a set of tools that allow you to take an implementation of the first form of a DSL block, one that takes a parameter, and turn it into an implementation of the second form, one that doesn't take a parameter.
Suppose you wanted to write a simple DSL block that takes a parameter:
configure_me do |config| config.add_foo(1) config.add_bar(2) end
You could write this as follows:
class ConfigMethods def add_foo(value) # do something end def add_bar(value) # do something end end def configure_me yield ConfigMethods.new end
That was easy. However, now suppose you wanted to support usage without the “config” parameter. e.g.
configure_me do add_foo(1) add_bar(2) end
With Blockenspiel, you can do this in two
quick steps. First, tell Blockenspiel that
your ConfigMethods
class is a DSL.
class ConfigMethods include Blockenspiel::DSL # <--- Add this line def add_foo(value) # do something end def add_bar(value) # do something end end
Next, write your configure_me
method using Blockenspiel:
def configure_me(&block) Blockenspiel.invoke(block, ConfigMethods.new) end
Now, your configure_me
method supports both DSL block
forms. A caller can opt to use the first form, with a parameter, simply by
providing a block that takes a parameter. Or, if the caller provides a
block that doesn't take a parameter, the second form without a
parameter is used.
How does that help me? (Or, why not just use instance_eval?)¶ ↑
As noted earlier, some libraries that provide parameter-less DSL blocks use
a simple instance_eval
, and they could even support both the
parameter and parameter-less mechanisms by checking the block arity:
def configure_me(&block) if block.arity == 1 yield ConfigMethods.new else ConfigMethods.new.instance_eval(&block) end end
That seems like a simple and effective technique that doesn't require a
separate library, so why use Blockenspiel?
Because instance_eval
introduces a number of surprising
problems. I discuss these issues in detail in ImplementingDSLblocks.rdoc, but
just to get your feet wet, suppose the caller wanted to call its own
methods inside the block:
def callers_helper_method # ... end configure_me do add_foo(1) callers_helper_method # Error! self is now an instance of ConfigMethods # so this will fail with a NameError add_bar(2) end
Blockenspiel employs a number of techniques
to mitigate the ill effects of instance_eval
. It delegates
methods that are not part of the DSL, back to the enclosing context object,
so that the caller retains access to helper methods. It also includes an
optional experimental technique (not available on all ruby platforms) that
temporarily mixes the DSL methods directly into the caller's
self
object, so that instance variable access is retained.
Is that it?¶ ↑
Although the basic usage is very simple, Blockenspiel is designed to be comprehensive. It supports all the use cases that I've run into during my own implementation of DSL blocks. Notably:
By default, Blockenspiel lets the caller choose to use a parametered block or a parameterless block, based on whether or not the block actually takes a parameter. You can also disable one or the other, to force the use of either a parametered or parameterless block.
You can also let the caller use your DSL by passing you a string or a file rather than a block. That is, you can create file-based DSLs such as the Rails routes file.
You can control wich methods of the class are available from parameterless blocks, and/or make some methods available under different names. Here are a few examples:
class ConfigMethods include Blockenspiel::DSL def add_foo # automatically added to the dsl # do stuff... end def my_private_method # do stuff... end dsl_method :my_private_method, false # remove from the dsl dsl_methods false # stop automatically adding methods to the dsl def another_private_method # not added # do stuff... end dsl_methods true # resume automatically adding methods to the dsl def add_bar # this method is automatically added # do stuff... end def add_baz # do stuff end dsl_method :add_baz_in_dsl, :add_baz # Method named differently # in a parameterless block end
This is also useful, for example, when you use attr_writer
.
Parameterless blocks do not support attr_writer
(or, by
corollary, attr_accessor
) well because methods with names of
the form “attribute=” are syntactically indistinguishable from variable
assignments:
configure_me do |config| config.foo = 1 # works fine when the block has a parameter end configure_me do # foo = 1 # <--- Doesn't work: looks like a variable assignment set_foo(1) # <--- Fix it by renaming to this instead end # This is implemented like this:: class ConfigMethods include Blockenspiel::DSL attr_writer :foo dsl_method :set_foo, :foo= # Make "foo=" available as "set_foo" end
This is in fact a common enough case that Blockenspiel includes conveninence tools for a DSL-friendly attr_writer and attr_accessor, providing an alternate syntax for setting attributes within a parameterless block:
configure_me do # foo = 1 # This syntax wouldn't work, but foo 1 # this syntax is now supported. puts "foo is #{foo}" # The getter still works. end # This is implemented like this:: class ConfigMethods include Blockenspiel::DSL dsl_attr_accessor :foo # DSL-friendly attr_accessor end
In some cases, you might want to dynamically generate a DSL object rather than defining a static class. Blockenspiel provides a tool to do just that. Here's an example:
Blockenspiel.invoke(block) do add_method(:set_foo) do |value| my_foo = value end add_method(:set_things_using_block) do |value, &blk| my_foo = value my_bar = blk.call end end
That API is itself a DSL block, and yes, Blockenspiel uses itself to implement this feature.
By default Blockenspiel uses mixins, which
usually exhibit fairly safe and non-surprising behavior. However, there are
a few cases when you might want the instance_eval
behavior
anyway. RSpec is a good example of such a case, since the DSL is being used
to construct objects, so it makes sense for instance variables inside the
block to belong to the object being constructed. Blockenspiel gives you the option of choosing
instance_eval
in case you need it. Blockenspiel also provides a compromise
behavior that uses a proxy to dispatch methods to the DSL object or the
block's context.
Blockenspiel also correctly handles nested blocks. e.g.
configure_me do set_foo(1) configure_another do # A block within another block set_bar(2) configure_another do # A block within itself set_bar(3) end end end
Blockenspiel provides three strategies for doing parameterless DSL blocks.
The default strategy uses a proxy object that delegates unrecognized methods out to the calling context. It should work well for most cases.
Second, some applications might want to use the simple
instance_eval
behavior. RSpec is a good example of such a
case, since the DSL is being used to construct objects, so it makes sense
for instance variables inside the block to belong to the object being
constructed.
Third, an experimental mixin strategy is provided, which adds the DSL methods directly to the context's self object, and removes them afterward. This is available on Rubinius and JRuby but not on MRI.
Finally, Blockenspiel is thread safe, correctly handling, for example, the case of multiple threads trying to mix methods into the same object concurrently.
Requirements¶ ↑
-
Ruby 1.9.3 or later, JRuby 1.5 or later, or Rubinius 1.0 or later.
Installation¶ ↑
gem install blockenspiel
Known issues and to-do items¶ ↑
-
Implementing wildcard DSL methods using
method_missing
doesn't work. I haven't yet decided on the right semantics for this case, or whether it is even a reasonable feature at all. -
Including Blockenspiel::DSL in a module (rather than a class) is not yet supported, but this is planned for a future release.
-
Find a way to implement mixin behavior reliably on MRI.
Development and support¶ ↑
Documentation is available at dazuma.github.com/blockenspiel/rdoc
Source code is hosted on Github at github.com/dazuma/blockenspiel
Contributions are welcome. Fork the project on Github.
Report bugs on Github issues at github.org/dazuma/blockenspiel/issues
Contact the author at dazuma at gmail dot com.
Author / Credits¶ ↑
Blockenspiel is written by Daniel Azuma (www.daniel-azuma.com/).
The mixin implementation is based on a concept by the late Why The Lucky Stiff, documented in his 6 October 2008 blog posting entitled “Mixing Our Way Out Of Instance Eval?”. The original link is gone, but you may find copies or mirrors out there.
The unmixer code is based on Mixology, version by Patrick Farley, anonymous z, Dan Manges, and Clint Bishop. The JRuby code is adapted from Mixology 0.1, and has been stripped down and modified to support JRuby >= 1.2. The Rubinius code was adapted from unreleased code in the Mixology source tree and modified to support Rubinius 1.0. I know Mixology 0.2 is now available, but its Rubinius support is not active, and I'd rather keep the unmixer bundled with Blockenspiel for now to reduce dependencies. Earlier versions of Blockenspiel also included a C extension, adapted from Mixology, to support mixins for MRI, but this code has been disabled due to issues with newer versions of Ruby.
The dsl_attr_writer and dsl_attr_accessor feature came from a suggestion by Luis Lavena.
License¶ ↑
Copyright 2008 Daniel Azuma.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-
Neither the name of the copyright holder, nor the names of any other contributors to this software, may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.