blockenspiel 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -2
- data/ImplementingDSLblocks.txt +493 -449
- data/README.txt +7 -6
- data/lib/blockenspiel.rb +2 -1
- metadata +2 -2
data/History.txt
CHANGED
data/ImplementingDSLblocks.txt
CHANGED
@@ -2,82 +2,84 @@
|
|
2
2
|
|
3
3
|
by Daniel Azuma, 19 October 2008
|
4
4
|
|
5
|
-
|
5
|
+
<em>DSL blocks</em> are constructs commonly used in APIs written in Ruby. In this paper I present an overview of the implementation strategies proposed for this important pattern. I will first describe the features of DSL blocks, utilizing illustrations from several well-known Ruby libraries. I will then survey and critique five implementation strategies that have been put forth. Finally, I will present a new library, {Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel], designed to be a comprehensive implementation of DSL blocks.
|
6
6
|
|
7
7
|
=== An illustrative overview of DSL blocks
|
8
8
|
|
9
|
-
If you've done
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
Or perhaps you've used the
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Or perhaps you've described testing scenarios
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
Or perhaps you
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
9
|
+
If you've done much Ruby programming, chances are you've run into mini-DSLs (Domain-Specific Languages) that live inside blocks. Perhaps you've encountered them in Ruby standard library calls, such as <tt>File#open</tt>, a call that lets you interact with a stream while performing automatic setup and cleanup for you:
|
10
|
+
|
11
|
+
File.open("myfile.txt") do |io|
|
12
|
+
io.each_line do |line|
|
13
|
+
puts line unless line =~ /^\s*#/
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Or perhaps you've used the XML {builder}[http://builder.rubyforge.org/] library, which uses nested blocks to match the structure of the XML being generated:
|
18
|
+
|
19
|
+
builder = Builder::XmlMarkup.new
|
20
|
+
builder.page do
|
21
|
+
builder.element1('hello')
|
22
|
+
builder.element2('world')
|
23
|
+
builder.collection do
|
24
|
+
builder.interior do
|
25
|
+
builder.element3('foo')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Or perhaps you've described testing scenarios using {RSpec}[http://rspec.info/], building and documenting test cases using English-sounding commands such as "describe" and "it_should_behave_like":
|
31
|
+
|
32
|
+
describe Stack do
|
33
|
+
|
34
|
+
before(:each) do
|
35
|
+
@stack = Stack.new
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "(empty)" do
|
39
|
+
|
40
|
+
it { @stack.should be_empty }
|
41
|
+
|
42
|
+
it_should_behave_like "non-full Stack"
|
43
|
+
|
44
|
+
it "should complain when sent #peek" do
|
45
|
+
lambda { @stack.peek }.should raise_error(StackUnderflowError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should complain when sent #pop" do
|
49
|
+
lambda { @stack.pop }.should raise_error(StackUnderflowError)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
# etc...
|
55
|
+
|
56
|
+
Or perhaps you were introduced to Ruby via the {Rails}[http://www.rubyonrails.org/] framework, and you're used to setting up configurations via blocks:
|
57
|
+
|
58
|
+
ActionController::Routing::Routes.draw do |map|
|
59
|
+
map.connect ':controller/:action/:id'
|
60
|
+
map.connect ':controller/:action/:page/:format'
|
61
|
+
# etc...
|
62
|
+
end
|
63
|
+
|
64
|
+
Rails::Initializer.run do |config|
|
65
|
+
config.time_zone = 'UTC'
|
66
|
+
config.log_level = :debug
|
67
|
+
# etc...
|
68
|
+
end
|
69
69
|
|
70
70
|
Blocks are central to Ruby as a language, and it feels natural to Ruby programmers to use them to delimit specialized code. When designing an API for a Ruby library, blocks like these are, in many cases, a natural and effective pattern.
|
71
71
|
|
72
|
-
===
|
72
|
+
=== Let's be more precise about what we mean by "DSL block".
|
73
73
|
|
74
|
-
Blocks in Ruby are used for a variety of purposes. In many cases, they are used to provide _callbacks_, specifying functionality to inject into an operation. A simple example is the +each+ method, which iterates over a collection, using the given block as a callback that allows the caller to specify processing to perform on each element.
|
74
|
+
Blocks in Ruby are used for a variety of purposes. In many cases, they are used to provide _callbacks_, specifying functionality to inject into an operation. If you come from a functional programming background, you might see them as lambda expressions. A simple example is the +each+ method, which iterates over a collection, using the given block as a callback that allows the caller to specify processing to perform on each element.
|
75
|
+
|
76
|
+
When we speak of DSL blocks, we are describing something conceptually and semanticaly different. Rather than looking for a specification of _functionality_, the method wants to provide the caller with a _language_ to _describe_ something. The block merely serves as a space in which to use that language.
|
75
77
|
|
76
78
|
Consider the Rails Routing example above. The Rails application needs to specify how URLs should be interpreted as commands sent to controllers, and, conversely, how command descriptions should be expressed as URLs. Rails thus defines a language that can be used to describe these mappings. The language uses the "connect" verb, a string with embedded codes describing the URL's various parts, and optional parameters that specify further details about the mapping.
|
77
79
|
|
78
80
|
The Rails Initializer illustrates another common pattern: that of using a DSL block to perform extended configuration of the method call. Again, a language is being defined here: certain property names such as "time_zone" have meanings understood by the Rails framework.
|
79
81
|
|
80
|
-
Note that in both this case and the Routing case,
|
82
|
+
Note that in both this case and the Routing case, the information contained in the block is descriptive. It is possible to imagine a syntax in which all the necessary information is passed into the method (<tt>Routes#draw</tt> or <tt>Initializer#run</tt>) as parameters. However, a block-based language makes the code much more readable. Rather than trying to express complex information in a parameter list, a block containing descriptive declarations is specified and executed.
|
81
83
|
|
82
84
|
The RSpec example illustrates a more sophisticated case with many keywords and multiple levels of blocks, but it shares common features with the Rails examples. Again, a language is being defined to describe things that could conceivably have been passed in as parameters, but are being specified in a block for clarity and readability.
|
83
85
|
|
@@ -88,47 +90,47 @@ So far, we can see that DSL blocks have the following properties:
|
|
88
90
|
* A method accepts a block from the caller, and executes the block exactly once.
|
89
91
|
* The domain-specific language is available to the caller lexically within the block.
|
90
92
|
|
91
|
-
As far as I have been able to determine, the term "DSL block" originated in 2007 with a {blog post}[http://blog.8thlight.com/articles/2007/05/20/] by Micah Martin. In it, he describes a way to implement certain types of DSL blocks using <tt>instance_eval</tt>, calling the technique the "DSL block pattern". We will discuss the nuances of the <tt>instance_eval</tt> implementation in greater detail below. But first, let us ease into implementation by describing a simple strategy that has worked very well for many libraries, including Rails.
|
93
|
+
As far as I have been able to determine, the term "DSL block" originated in 2007 with a {blog post}[http://blog.8thlight.com/articles/2007/05/20/] by Micah Martin. In it, he describes a way to implement certain types of DSL blocks using <tt>instance_eval</tt>, calling the technique the "DSL block pattern". We will discuss the nuances of the <tt>instance_eval</tt> implementation in greater detail below. But first, let us ease into the implementation discussion by describing a simple strategy that has worked very well for many libraries, including Rails.
|
92
94
|
|
93
95
|
=== Implementation strategy 1: block parameters
|
94
96
|
|
95
|
-
In 2006, Jamis Buck posted a set of articles describing the Rails routing implementation. Tucked away at the top the {first article}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl] is a code snippet showing the DSL block implementation for Rails routing. This code, along with some of its context from the file <tt>action_controller/routing/route_set.rb</tt>, is listed below.
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
97
|
+
In 2006, Jamis Buck, one of the Rails core developers, posted a set of articles describing the Rails routing implementation. Tucked away at the top the {first article}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl] is a code snippet showing the DSL block implementation for Rails routing. This code, along with some of its context from the file <tt>action_controller/routing/route_set.rb</tt>, is listed below.
|
98
|
+
|
99
|
+
class RouteSet
|
100
|
+
|
101
|
+
class Mapper
|
102
|
+
def initialize(set)
|
103
|
+
@set = set
|
104
|
+
end
|
105
|
+
|
106
|
+
def connect(path, options = {})
|
107
|
+
@set.add_route(path, options)
|
108
|
+
end
|
109
|
+
# ...
|
110
|
+
end
|
111
|
+
|
112
|
+
# ...
|
113
|
+
|
114
|
+
def draw
|
115
|
+
clear!
|
116
|
+
yield Mapper.new(self)
|
117
|
+
named_routes.install
|
118
|
+
end
|
119
|
+
|
120
|
+
# ...
|
121
|
+
|
122
|
+
def add_route(path, options = {})
|
123
|
+
# ...
|
122
124
|
|
123
125
|
Recall how we specify routes in Rails: we call the +draw+ method, and pass it a block. The block receives a parameter that we call "+map+". We can then create routes by calling the +connect+ method on the parameter.
|
124
126
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
127
|
+
ActionController::Routing::Routes.draw do |map|
|
128
|
+
map.connect ':controller/:action/:id'
|
129
|
+
map.connect ':controller/:action/:page/:format'
|
130
|
+
# etc.
|
131
|
+
end
|
130
132
|
|
131
|
-
It should be fairly easy to see how the code above accomplishes this. The +draw+ method creates an object of class +Mapper+. The
|
133
|
+
It should be fairly easy to see how the code above accomplishes this. The +draw+ method creates an object of class +Mapper+. The +Mapper+ class defines the domain-specific language, in particular the +connect+ method that we are familiar with. Note how its implementation is simply to proxy calls into the routing system: it keeps an instance variable called "<tt>@set</tt>" that points back at the +RouteSet+ we are modifying. Then, +draw+ yields the mapper instance back to the block, where we receive it as our +map+ variable.
|
132
134
|
|
133
135
|
A large number of DSL block implementations are variations on this theme. We define a proxy class (+Mapper+ in this case) that exposes the domain-specific language we want and communicates back to the system we are describing. We then yield an instance of that proxy back to the block, which receives it as a parameter. The block then manipulates the DSL using its parameter.
|
134
136
|
|
@@ -136,62 +138,62 @@ This pattern is extremely powerful and pervasive. It is simple and clean to impl
|
|
136
138
|
|
137
139
|
However, some have argued that it is too verbose. Why, in a DSL, is it necessary to litter the entire block with references to the block variable? If we know that the caller is supposed to be interacting with the DSL in the block, is it really necessary to have the explicit parameter? Perhaps Rails routing, for example, could be specified more succinctly like the following, in which the +map+ variable is implied.
|
138
140
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
The differences become even more clear if you have nested blocks. Because Ruby 1.8
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
141
|
+
ActionController::Routing::Routes.draw do
|
142
|
+
connect ':controller/:action/:id'
|
143
|
+
connect ':controller/:action/:page/:format'
|
144
|
+
# etc.
|
145
|
+
end
|
146
|
+
|
147
|
+
The differences become even more clear if you have nested blocks. Because of Ruby 1.8's scoping semantics, nested blocks need different variable names. Consider an imaginary DSL block that looks like this:
|
148
|
+
|
149
|
+
create_container do |container|
|
150
|
+
container.create_subcontainer do |subcontainer1|
|
151
|
+
subcontainer1.create_subcontainer do |subcontainer2|
|
152
|
+
subcontainer2.create_object do |objconfig|
|
153
|
+
objconfig.set_value(3)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
subcontainer1.create_subcontainer do |subcontainer3|
|
157
|
+
subcontainer3.create_object do |objconfig2|
|
158
|
+
objconfig2.set_value(1)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
That was clunky. Wouldn't it be nice to instead see this?...
|
165
|
+
|
166
|
+
create_container do
|
167
|
+
create_subcontainer do
|
168
|
+
create_subcontainer do
|
169
|
+
create_object do
|
170
|
+
set_value(3)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
create_subcontainer do
|
174
|
+
create_object do
|
175
|
+
set_value(1)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
178
180
|
|
179
181
|
In the next section we examine an alternate implementation that supports such usage. But first, let us summarize our discussion of the "block parameter" implementation.
|
180
182
|
|
181
183
|
*Implementation*:
|
182
184
|
|
183
|
-
* Create a proxy class defining the DSL
|
185
|
+
* Create a proxy class defining the DSL.
|
184
186
|
* Yield the proxy object to the block as a parameter.
|
185
187
|
|
186
188
|
*Pros*:
|
187
189
|
|
188
|
-
* Easy to implement
|
189
|
-
* Clear syntax for the caller
|
190
|
-
* Clear separation between the DSL and surrounding code
|
190
|
+
* Easy to implement.
|
191
|
+
* Clear syntax for the caller.
|
192
|
+
* Clear separation between the DSL and surrounding code.
|
191
193
|
|
192
194
|
*Cons*:
|
193
195
|
|
194
|
-
* Verbose: requires a block parameter
|
196
|
+
* Verbose: requires a block parameter, sometimes resulting in clumsy syntax.
|
195
197
|
|
196
198
|
*Verdict*: Use it if you want a simple, effective DSL block and don't mind requiring a parameter.
|
197
199
|
|
@@ -201,312 +203,314 @@ Micah Martin's post[http://blog.8thlight.com/articles/2007/05/20/] describes an
|
|
201
203
|
|
202
204
|
It is perhaps instructive to see an example. Let's create a simple class
|
203
205
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
Things to note here is that the method +foo+ and the instance variable <tt>@instvar</tt> are defined on instances of +
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
How does this help us? Notice that within the <tt>instance_eval</tt> block, the methods of +
|
206
|
+
Class MyClass
|
207
|
+
def initialize
|
208
|
+
@instvar = 1
|
209
|
+
end
|
210
|
+
def foo
|
211
|
+
puts "in foo"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
Things to note here is that the method +foo+ and the instance variable <tt>@instvar</tt> are defined on instances of +MyClass+. Now let's <tt>instance_eval</tt> an instance of +MyClass+ from another class.
|
216
|
+
|
217
|
+
class Tester
|
218
|
+
def test
|
219
|
+
puts @instvar.inspect # prints "nil" since the Tester object has no @instvar
|
220
|
+
x = MyClass.new # create a new instance of MyClass
|
221
|
+
x.instance_eval do # change self to point to x during the block
|
222
|
+
puts @instvar.inspect # prints "1" since self now points at x
|
223
|
+
@instvar = 2 # changes x's @instvar to 2
|
224
|
+
foo # calls x's foo and prints "in foo"
|
225
|
+
puts x == self # prints "true". The local variable x is still accessible
|
226
|
+
end # end of the block. self is now back to the Tester instance
|
227
|
+
puts @instvar.inspect # prints "nil" since Tester still has no @instvar
|
228
|
+
foo # NameError since Tester has no foo method.
|
229
|
+
end
|
230
|
+
end
|
231
|
+
Tester.new.test # Runs the above test
|
232
|
+
|
233
|
+
How does this help us? Notice that within the <tt>instance_eval</tt> block, the methods of +x+ can be called without explicitly naming +x+ because the +self+ reference points to +x+. So in the Rails Routing example, if we used <tt>instance_eval</tt> to get +self+ to point to the +Mapper+ instance in the block, then we wouldn't need to pass it explicitly as a parameter, and the block could call methods on it without explicitly naming it.
|
232
234
|
|
233
235
|
Here is a revised version of the Rails routing code:
|
234
236
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
This modified version of the routing API now no longer requires a block parameter, and the DSL is correspondingly more succinct. Sounds like a win all around, right?
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
237
|
+
class RouteSet
|
238
|
+
|
239
|
+
class Mapper
|
240
|
+
def initialize(set)
|
241
|
+
@set = set
|
242
|
+
end
|
243
|
+
|
244
|
+
def connect(path, options = {})
|
245
|
+
@set.add_route(path, options)
|
246
|
+
end
|
247
|
+
# ...
|
248
|
+
end
|
249
|
+
|
250
|
+
# ...
|
251
|
+
|
252
|
+
# We need to pass the block itself to instance_eval, so get it
|
253
|
+
# as a parameter to the draw method.
|
254
|
+
def draw(&block)
|
255
|
+
clear!
|
256
|
+
map = Mapper.new(self) # Create the proxy object as before
|
257
|
+
map.instance_eval(&block) # Call the block, setting self to point to map.
|
258
|
+
named_routes.install
|
259
|
+
end
|
260
|
+
|
261
|
+
# ...
|
262
|
+
|
263
|
+
def add_route(path, options = {})
|
264
|
+
# ...
|
265
|
+
|
266
|
+
This modified version of the routing API now no longer requires a block parameter, and the DSL is correspondingly more succinct. Sounds like a win all around, right?
|
267
|
+
|
268
|
+
Well, not so fast. Our implementation here has a number of subtle and surprising side effects. Suppose, for instance, we were to write a little helper method to help us generate URLs:
|
269
|
+
|
270
|
+
URL_PREFIX = 'mywebsite/:controller/:action/'
|
271
|
+
def makeurl(*params)
|
272
|
+
URL_PREFIX + params.map{ |e| e.inspect }.join('/')
|
273
|
+
end
|
274
|
+
|
275
|
+
Using the above method, it becomes easy to generate URL strings:
|
276
|
+
|
277
|
+
makeurl(:id, :style) # --> "mywebsite/:controller/:action/:id/:style"
|
274
278
|
|
275
279
|
Our <tt>routes.rb</tt> file, utilizing our "improvement" to the routing DSL, might now like this:
|
276
280
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
281
|
+
URL_PREFIX = 'mywebsite/:controller/:action/'
|
282
|
+
def makeurl(*params)
|
283
|
+
URL_PREFIX + params.map{ |e| e.inspect }.join('/')
|
284
|
+
end
|
285
|
+
|
286
|
+
ActionController::Routing::Routes.draw do
|
287
|
+
connect makeurl :id
|
288
|
+
connect makeurl :page, :format
|
289
|
+
# etc.
|
290
|
+
end
|
287
291
|
|
288
292
|
Looks nice, right? Except that when we try to run it, we get:
|
289
293
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
+
NoMethodError: undefined method `[]' for :id:Symbol
|
295
|
+
from /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/routing/builder.rb:168:in `build'
|
296
|
+
from /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/routing/route_set.rb:261:in `add_route'
|
297
|
+
...
|
294
298
|
|
295
|
-
What's up with that cryptic error? After some furious digging into the guts of Rails, we discover to our surprise
|
299
|
+
What's up with that cryptic error? After some furious digging into the guts of Rails, we discover to our surprise Ruby is trying to call +makeurl+ on the <em>+Mapper+</em> object, rather than calling our +makeurl+ helper method. And then it dawns on us. We used <tt>instance_eval</tt> to change +self+ to point to the +Mapper+ proxy inside the block, and it did exactly what we asked. It let us call the +connect+ method on the +Mapper+ without having to pass it in as a block parameter. But it similarly also tried to call +makeurl+ on the +Mapper+. The helper method we so cleverly wrote is being bypassed.
|
296
300
|
|
297
301
|
The problem gets worse. Changing +self+ affects not only how methods are looked up, but also how instance variables are looked up. For example, we are now able to do this:
|
298
302
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
303
|
+
ActionController::Routing::Routes.draw do
|
304
|
+
@set = nil
|
305
|
+
connect ':controller/:action/:id' # Exception raised here!
|
306
|
+
connect ':controller/:action/:page/:format'
|
307
|
+
# etc.
|
308
|
+
end
|
305
309
|
|
306
|
-
What happened? If we recall, <tt>@set</tt> is used by the +Mapper+ object to point back to the routing +RouteSet+. It is how the proxy knows what it is proxying for. But since we've used <tt>instance_eval</tt>, we now have free rein over the +Mapper+ object's internal instance variables, including the ability to clobber them. Furthermore, maybe we were actually expecting to access our own <tt>@set</tt> variable, and we haven't done that. Any instance variables from the caller's closure are no longer accessible inside the block.
|
310
|
+
What happened? If we recall, <tt>@set</tt> is used by the +Mapper+ object to point back to the routing +RouteSet+. It is how the proxy knows what it is proxying for. But since we've used <tt>instance_eval</tt>, we now have free rein over the +Mapper+ object's internal instance variables, including the ability to clobber them. And that's precisely what we did here. Furthermore, maybe we were actually expecting to access our own <tt>@set</tt> variable, and we haven't done that. Any instance variables from the caller's closure are in fact no longer accessible inside the block.
|
307
311
|
|
308
|
-
The problem gets even worse. If we think about the cryptic error message we got when we tried to use our +makeurl+ helper method,
|
312
|
+
The problem gets even worse. If we think about the cryptic error message we got when we tried to use our +makeurl+ helper method, we begin to realize that we've run into an ambiguity inherent in dropping the block parameter. If +self+ has changed inside the block, and we tried to call +makeurl+, wouldn't we expect a +NoMethodError+ to be raised for +makeurl+ on the +Mapper+ class, rather than for "<tt>[]</tt>" on the +Symbol+ class? What happened? Then we remember that Rails's routing DSL supports named routes. You do not have to call the specific +connect+ method to create a route. In fact, you can call _any_ method name. It is thus ambiguous, when we invoke +makeurl+, whether we mean our helper method or a named route called "makeurl". Rails assumed we meant the named route, but in fact that isn't what we had intended.
|
309
313
|
|
310
|
-
This all sounds pretty bad. Do we give up on <tt>instance_eval</tt>? Some members of the Ruby community have, and indeed the technique has generally fallen out of favor in major libraries. Jim Weirich originally[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] utilized <tt>instance_eval</tt> in the XML Builder library illustrated
|
314
|
+
This all sounds pretty bad. Do we give up on <tt>instance_eval</tt>? Some members of the Ruby community have, and indeed the technique has generally fallen out of favor in major libraries. Jim Weirich, for instance, {originally}[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] utilized <tt>instance_eval</tt> in the XML Builder library illustrated earlier, but later deprecated and removed it because of its surprising behavior.
|
311
315
|
|
312
|
-
There are, however, a few specific exceptions. RSpec's DSL is intended as a class-constructive language: it constructs ruby classes behind the scenes. In the RSpec example at the beginning of this paper, you may notice the use of the <tt>@stack</tt> instance variable. In fact, this is intended as an instance variable of the RSpec test story being written, and as such, <tt>instance_eval</tt> is required because of the kind of language that RSpec wants to use. But in more common cases, such as specifying configuration, <tt>instance_eval</tt> does not give us the most desirable behavior. The general consensus now, expressed for example in recent articles from Why[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html] and {Ola Bini}[http://olabini.com/blog/2008/09/dont-overuse-instance_eval-and-instance_exec/], is that it should be avoided.
|
316
|
+
There are, however, a few specific exceptions. RSpec's DSL is intended as a class-constructive language: it constructs ruby classes behind the scenes. In the RSpec example at the beginning of this paper, you may notice the use of the <tt>@stack</tt> instance variable. In fact, this is intended as an instance variable of the RSpec test story being written, and as such, <tt>instance_eval</tt> is required because of the kind of language that RSpec wants to use. But in more common cases, such as specifying configuration, <tt>instance_eval</tt> does not give us the most desirable behavior. The general consensus now, expressed for example in recent articles from {Why}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html] and {Ola Bini}[http://olabini.com/blog/2008/09/dont-overuse-instance_eval-and-instance_exec/], is that it should be avoided.
|
313
317
|
|
314
318
|
So does this mean we're stuck with block parameters for better or worse? Not quite. Several alternatives have been proposed recently, and we'll take a look at them in the next few sections. But first, let's summarize the discussion of <tt>instance_eval</tt>.
|
315
319
|
|
316
320
|
*Implementation*:
|
317
321
|
|
318
|
-
* Create a proxy class defining the DSL
|
322
|
+
* Create a proxy class defining the DSL.
|
319
323
|
* Use <tt>instance_eval</tt> to change +self+ to the proxy in the block.
|
320
324
|
|
321
325
|
*Pros*:
|
322
326
|
|
323
|
-
* Easy to implement
|
324
|
-
* Concise: does not require a block parameter
|
325
|
-
* Useful for class-constructive DSLs
|
327
|
+
* Easy to implement.
|
328
|
+
* Concise: does not require a block parameter.
|
329
|
+
* Useful for class-constructive DSLs.
|
326
330
|
|
327
331
|
*Cons*:
|
328
332
|
|
329
|
-
* Surprising lookup behavior for helper methods
|
330
|
-
* Surprising lookup behavior for instance variables
|
331
|
-
* Breaks encapuslation of proxy class
|
332
|
-
* Possibility of a helper method vs DSL method ambiguity
|
333
|
+
* Surprising lookup behavior for helper methods.
|
334
|
+
* Surprising lookup behavior for instance variables.
|
335
|
+
* Breaks encapuslation of the proxy class.
|
336
|
+
* Possibility of a helper method vs DSL method ambiguity.
|
333
337
|
|
334
338
|
*Verdict*: Use it if you are writing a DSL that constructs classes or modifies class internals. Otherwise avoid it. There are better alternatives.
|
335
339
|
|
336
340
|
=== Implementation strategy 3: delegation
|
337
341
|
|
338
|
-
In our discussion of <tt>instance_eval</tt>, a major problem we identified is that helper methods,
|
342
|
+
In our discussion of <tt>instance_eval</tt>, a major problem we identified is that helper methods, and indeed all other methods from the calling context, are not available within the block. One way to improve the situation, perhaps, is by redirecting any methods not defined in the DSL (that is, not defined on the proxy object) back to the original context. That way, we still have access to our helper methods--they'll appear to be part of the DSL. This "delegation" approach was proposed by Dan Manges in his {blog}[http://www.dcmanges.com/blog/ruby-dsls-instance-eval-with-delegation].
|
339
343
|
|
340
344
|
The implementation here is not difficult, if we pull out another tool from Ruby's metaprogramming toolbox, <tt>method_missing</tt>. This method is called whenever you call a method that is not explicitly defined on an object's class. It provides a "last ditch" opportunity to handle the method before Ruby bails with a dreaded +NoMethodError+. Again, an example is probably useful here.
|
341
345
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
346
|
+
class MyClass
|
347
|
+
def foo
|
348
|
+
puts "in foo"
|
349
|
+
end
|
350
|
+
def method_missing(name, *params)
|
351
|
+
puts "last ditch method #{name.inspect} called with params: #{params.inspect}"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
x = MyClass.new
|
356
|
+
x.foo # prints "in foo"
|
357
|
+
x.bar # prints "last ditch method :bar called with params: []"
|
358
|
+
x.baz(1,2) # prints "last ditch method :baz called with params: [1,2]"
|
355
359
|
|
356
360
|
How does this help us? Well, our goal is to redirect any calls that aren't available in the DSL, back to the block's original context. To do that, we simply define <tt>method_missing</tt> on our proxy class. In that method, we delegate the call, using +send+, back to the original +self+ from the block's context.
|
357
361
|
|
358
|
-
The remaining trick is how to get the original +self+. This can be done with a little bit of hackery if we realize that any +Proc+ object lets you access the
|
362
|
+
The remaining trick is how to get the block's original +self+. This can be done with a little bit of hackery if we realize that any +Proc+ object lets you access the binding of the context where it came from. We can get the original +self+ reference by evaluating "self" in that binding.
|
359
363
|
|
360
364
|
Going back to our modification of the Rails routing code, let's see what this looks like.
|
361
365
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
Now people familiar with how Rails
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
While this would appear to have solved the helper method issue, it does nothing to address the other issues we encountered. For example, invoking instance variables inside the block will still reference instance variables of the Mapper proxy object. There is, as far as I know, no way to delegate instance variable lookup. By using <tt>instance_eval</tt>, we still break encapsulation of the proxy class. And we have not solved the fundamental ambiguity in the method names
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
The upshot is not much different from Manges's delegation technique. Method calls get delegated in approximately the same way (though Weirich speculates that +MethodDirector+'s dispatch process may be slow). Within the block, +self+ now points to the +MethodDirector+ object rather than the +Mapper+ object. This means that we're no longer breaking encapsulation of the mapper proxy (but we are breaking the +MethodDirector+
|
366
|
+
class RouteSet
|
367
|
+
|
368
|
+
class Mapper
|
369
|
+
# We save the block's original "self" reference also, so that we
|
370
|
+
# can redirect unhandled methods back to the original context.
|
371
|
+
def initialize(set, original_self)
|
372
|
+
@set = set
|
373
|
+
@original_self = original_self
|
374
|
+
end
|
375
|
+
|
376
|
+
def connect(path, options = {})
|
377
|
+
@set.add_route(path, options)
|
378
|
+
end
|
379
|
+
|
380
|
+
# ...
|
381
|
+
|
382
|
+
# Redirect all other methods
|
383
|
+
def method_missing(name, *params, &blk)
|
384
|
+
@original_self.send(name, *params, &blk)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
# ...
|
389
|
+
|
390
|
+
def draw(&block)
|
391
|
+
clear!
|
392
|
+
original_self = Kernel.eval('self', block.binding) # Get the block's context self
|
393
|
+
map = Mapper.new(self, original_self) # Give it to the proxy
|
394
|
+
map.instance_eval(&block)
|
395
|
+
named_routes.install
|
396
|
+
end
|
397
|
+
|
398
|
+
# ...
|
399
|
+
|
400
|
+
def add_route(path, options = {})
|
401
|
+
# ...
|
402
|
+
|
403
|
+
Now people familiar with how Rails is implemented will probably object that +Mapper+ already _has_ a <tt>method_missing</tt> defined. It's used to implement the named routes that caused the ambiguity we described earlier. By replacing Rails's <tt>method_missing</tt> with my own <tt>method_missing</tt>, I effectively disable named routes. Granted, I'm ignoring that issue right now, and just trying to illustrate how method delegation works. As long as we don't use named routes, our +makeurl+ example will now work as we expect:
|
404
|
+
|
405
|
+
URL_PREFIX = 'mywebsite/:controller/:action/'
|
406
|
+
def makeurl(*params)
|
407
|
+
URL_PREFIX + params.map{ |e| e.inspect }.join('/')
|
408
|
+
end
|
409
|
+
|
410
|
+
ActionController::Routing::Routes.draw do
|
411
|
+
connect makeurl :id
|
412
|
+
connect makeurl :page, :format
|
413
|
+
# etc.
|
414
|
+
end
|
415
|
+
|
416
|
+
While this would appear to have solved the helper method issue, it does nothing to address the other issues we encountered. For example, invoking instance variables inside the block will still reference the instance variables of the +Mapper+ proxy object. There is, as far as I know, no way to delegate instance variable lookup. By using <tt>instance_eval</tt>, we still break encapsulation of the proxy class. And we have not solved the fundamental ambiguity in the method names: whether "makeurl" _should_ be called as part of the DSL or as a helper method. Indeed, that ambiguity really is inherent to the goal of eliminating the block parameter. Without a block parameter, it becomes much harder, syntactically, to specify whether a method should be directed to the DSL or somewhere else.
|
417
|
+
|
418
|
+
Several variations on the delegation theme have been proposed. One such variation uses a technique proposed by Jim Weirich called {MethodDirector}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/19153]. In this variation, we create a small object whose sole purpose is to receive methods and delegate them to whatever object it thinks should handle them. Utilizing Jim's +MethodDirector+ implementation rather than adding a <tt>method_missing</tt> to our +Mapper+ proxy, we could rewrite the +draw+ method as follows:
|
419
|
+
|
420
|
+
def draw(&block)
|
421
|
+
clear!
|
422
|
+
original_self = Kernel.eval('self', block.binding) # Get the block's context self
|
423
|
+
map = Mapper.new(self) # Get the proxy
|
424
|
+
director = MethodDirector.new([map, original_self]) # Create a director
|
425
|
+
director.instance_eval(&block) # Use the director as self
|
426
|
+
named_routes.install
|
427
|
+
end
|
428
|
+
|
429
|
+
The upshot is not much different from Manges's delegation technique. Method calls get delegated in approximately the same way (though Weirich speculates that +MethodDirector+'s dispatch process may be slow). Within the block, +self+ now points to the +MethodDirector+ object rather than the +Mapper+ object. This means that we're no longer breaking encapsulation of the mapper proxy (but we are breaking the encapsulation of the +MethodDirector+ itself.) We still cannot access instance variables from the block's context. We no longer clobber +Mapper+'s instance variables, but now we can clobber +MethodDirector+'s. In short, it might be considered a slight improvement, but not much, at a possible performance cost.
|
426
430
|
|
427
431
|
Let's wrap up our discussion of delegation and then delve into some different, and perhaps more useful, ideas.
|
428
432
|
|
429
433
|
*Implementation*:
|
430
434
|
|
431
|
-
* Create a proxy class defining the DSL
|
435
|
+
* Create a proxy class defining the DSL.
|
432
436
|
* Use <tt>method_missing</tt> to delegate unhandled methods back to the block's context.
|
433
437
|
* Use <tt>instance_eval</tt> to change +self+ to the proxy in the block.
|
434
438
|
|
435
439
|
*Pros*:
|
436
440
|
|
437
|
-
* Concise: does not require a block parameter
|
438
|
-
* Better than a straight <tt>instance_eval</tt> in that it handles helper methods
|
441
|
+
* Concise: does not require a block parameter.
|
442
|
+
* Better than a straight <tt>instance_eval</tt> in that it handles helper methods.
|
439
443
|
|
440
444
|
*Cons*:
|
441
445
|
|
442
|
-
* Still exhibits surprising lookup behavior for instance variables
|
443
|
-
* Still breaks encapuslation of proxy class
|
444
|
-
* Does nothing to solve the helper method vs DSL method ambiguity
|
445
|
-
* Harder to implement than a simple <tt>instance_eval</tt
|
446
|
+
* Still exhibits surprising lookup behavior for instance variables.
|
447
|
+
* Still breaks encapuslation of the proxy class.
|
448
|
+
* Does nothing to solve the helper method vs DSL method ambiguity.
|
449
|
+
* Harder to implement than a simple <tt>instance_eval</tt>.
|
446
450
|
|
447
451
|
*Verdict*: Use it for cases where <tt>instance_eval</tt> is appropriate (i.e. if you are writing a DSL that constructs classes or modifies class internals) and are worried about helper methods being available. Otherwise avoid it.
|
448
452
|
|
449
453
|
=== Implementation strategy 4: arity detection
|
450
454
|
|
451
|
-
Intrigued by the discussion surrounding <tt>instance_eval</tt> and DSL blocks, James Edward Gray II (of {RubyQuiz}[http://rubyquiz.com/] fame) chimed in with a compromise. In his {blog}[http://blog.grayproductions.net/articles/dsl_block_styles], he argues that the the issue boils down to two basic strategies: block parameters and <tt>instance_eval</tt>, both of which have their own strengths and weaknesses. On one hand, block parameters avoid surprising behavior and ambiguity in exchange for somewhat more verbose syntax. On the other hand, <tt>instance_eval</tt> offers a more concise and perhaps more pleasing syntax in exchange for some
|
455
|
+
Intrigued by the discussion surrounding <tt>instance_eval</tt> and DSL blocks, James Edward Gray II (of {RubyQuiz}[http://rubyquiz.com/] fame) chimed in with a compromise. In his {blog}[http://blog.grayproductions.net/articles/dsl_block_styles], he argues that the the issue boils down to two basic strategies: block parameters and <tt>instance_eval</tt>, both of which have their own strengths and weaknesses. On one hand, block parameters avoid surprising behavior and ambiguity in exchange for somewhat more verbose syntax. On the other hand, <tt>instance_eval</tt> offers a more concise and perhaps more pleasing syntax in exchange for some ambiguity and surprising side effects. Neither solution is clearly better than the other, and either might be more appropriate in different circumstances. Thus, why not let the _caller_ decide which one to use?
|
452
456
|
|
453
|
-
This is in fact easier to do than we might think. When you call a method using a DSL block, you already make the choice to have your block take a parameter or not. The caller does one of the following:
|
457
|
+
This is in fact easier to do than we might think. When you call a method using a DSL block, you've already make the choice to have your block take a parameter or not. The caller does one of the following:
|
454
458
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
459
|
+
ActionController::Routing::Routes.draw do |map|
|
460
|
+
map.connect ':controller/:action/:id'
|
461
|
+
map.connect ':controller/:action/:page/:format'
|
462
|
+
# etc.
|
463
|
+
end
|
460
464
|
|
461
465
|
or
|
462
466
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
It is
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
467
|
+
ActionController::Routing::Routes.draw do
|
468
|
+
connect ':controller/:action/:id'
|
469
|
+
connect ':controller/:action/:page/:format'
|
470
|
+
# etc.
|
471
|
+
end
|
472
|
+
|
473
|
+
It is possible for the method itself to detect which case it is, just by examining the block. Every +Proc+ object provides a method called +arity+, which returns a notion of how many parameters the block expects. If you receive a block that expects a parameter, use the block parameter strategy; if you receive a block that doesn't expect a parmaeter, use <tt>instance_eval</tt> or one of its modifications. Under this technique, our Routing +draw+ method might look like this:
|
474
|
+
|
475
|
+
def draw(&block)
|
476
|
+
clear!
|
477
|
+
map = Mapper.new(self) # Create the proxy object as before
|
478
|
+
if block.arity == 1
|
479
|
+
block.call(map) # Block takes one parameter: use block parameter technique
|
480
|
+
else
|
481
|
+
map.instance_eval(&block) # otherwise, use instance_eval technique.
|
482
|
+
end
|
483
|
+
named_routes.install
|
484
|
+
end
|
481
485
|
|
482
486
|
Gray's proposal has a compelling advantage. The basis for the entire discussion is the suggestion that eliminating block parameters is desirable for the caller, and the objections raised are also, almost without exception, based on the experience of the caller. The basic question is thus whether the _caller_ ought to consider the benefits of eliminating block parameters to outweigh the costs. Therefore, it makes sense to put that choice in the hands of the caller rather than letting the library API designer dictate one choice or the other.
|
483
487
|
|
484
|
-
For example, one apparently inherent issue with a DSL block style that eliminates block parameters is the ambiguity between DSL methods and helper methods. By giving the caller the choice, we at once solve the ambiguity by providing a language for it. If the caller does not need to distinguish between the two, because she is not using helper methods or named routes, then she can choose to omit the block parameter and use <tt>instance_eval</tt> without harm. If, on the other hand, she
|
488
|
+
For example, one apparently inherent issue with a DSL block style that eliminates block parameters is the ambiguity between DSL methods and helper methods. By giving the caller the choice, we at once solve the ambiguity by providing a language for it. If the caller does not need to distinguish between the two, because she is not using helper methods or named routes, then she can choose to omit the block parameter and use <tt>instance_eval</tt> without harm. If, on the other hand, she _does_ need to distinguish between the two, as in the case of Rails routing where any method name could be a DSL method because of the named routes feature, then she can choose to make the block parameter explicit.
|
485
489
|
|
486
|
-
There is, however, a subtle disadvantage to providing the choice. By effectively allowing two DSL styles, a library that offers Gray's choice dilutes the identity and "branding" of its
|
490
|
+
There is, however, a subtle disadvantage to providing the choice. By effectively allowing two DSL styles, a library that offers Gray's choice dilutes the identity and "branding" of its DSL. If there are two "dialects" of the DSL, one that uses a block parameter and one that does not, it becomes harder for programmers to recognize the language. The two dialects might develop separate followings and distinct "best-practices" on account of their syntactic differences, and the schism would diminish the overall power of the DSL. While the actual cost of this diluting effect can be difficult to measure, it cannot be ignored, because the whole point of defining a DSL is to make code more understandable and recognizable.
|
487
491
|
|
488
|
-
Finally, there are some cases when one
|
492
|
+
Finally, there are some cases when one choice is specifically called for by the nature of the DSL being implemented. RSpec is a good example: it requires <tt>instance_eval</tt> in order to support access to the test story's instance variables. Allowing the caller to choose would not make sense in this case.
|
489
493
|
|
490
|
-
Let us summarize Gray's arity detection technique, and then proceed to an interesting new idea recently proposed by Why The
|
494
|
+
Let us summarize Gray's arity detection technique, and then proceed to an interesting new idea recently proposed by Why The Lucky Stiff.
|
491
495
|
|
492
496
|
*Implementation*:
|
493
497
|
|
494
|
-
* Create a proxy class defining the DSL
|
498
|
+
* Create a proxy class defining the DSL.
|
495
499
|
* Detect the choice of the caller based on block arity.
|
496
|
-
* Use either a block parameter or <tt>instance_eval</tt> to invoke the block
|
500
|
+
* Use either a block parameter or <tt>instance_eval</tt> to invoke the block.
|
497
501
|
|
498
502
|
*Pros*:
|
499
503
|
|
500
504
|
* Best of both worlds.
|
501
|
-
* Puts the choice in the hands of the caller, who can best make the decision
|
505
|
+
* Puts the choice in the hands of the caller, who can best make the decision.
|
502
506
|
* Implementation cost is not significant.
|
503
507
|
|
504
508
|
*Cons*:
|
505
509
|
|
506
|
-
* Not an all-encompassing solution-- either choice still has its own pros and cons
|
510
|
+
* Not an all-encompassing solution-- either choice still has its own pros and cons.
|
507
511
|
* Possibility of dilution of DSL branding.
|
508
512
|
|
509
|
-
*Verdict*: Use it for most cases when it is not clear whether block parameters or <tt>instance_eval</tt> is
|
513
|
+
*Verdict*: Use it for most cases when it is not clear whether block parameters or <tt>instance_eval</tt> is better.
|
510
514
|
|
511
515
|
=== Implementation strategy 5: mixins
|
512
516
|
|
@@ -516,91 +520,92 @@ Implementing this is actually harder than it sounds. We need to take the block c
|
|
516
520
|
|
517
521
|
Ruby provides tools for dynamically defining methods on and removing methods from an existing module. We might be tempted to try something like this:
|
518
522
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
523
|
+
def draw(&block)
|
524
|
+
clear!
|
525
|
+
save_self = self
|
526
|
+
original_self = Kernel.eval('self', block.binding)
|
527
|
+
original_self.class.module_eval do
|
528
|
+
define_method(:connect) do |path,options|
|
529
|
+
save_self.add_route(path,options)
|
530
|
+
end
|
531
|
+
end
|
532
|
+
yield
|
533
|
+
original_self.class.module_eval do
|
534
|
+
remove_method(:connect)
|
535
|
+
end
|
536
|
+
named_routes.install
|
537
|
+
end
|
534
538
|
|
535
539
|
This implementation, however, is fraught with problems. Notably, we are modifying the entire class of objects, including instances other than <tt>original_self</tt>, which is probably not what we intended. In addition, we could be unknowingly clobbering another +connect+ method defined on <tt>original_self</tt>'s class. (There are, of course, many other problems that I'm just ignoring for the sake of clarity, such as exception safety, and the fact that the +options+ parameter cannot take a default value when using <tt>define_method</tt>. Suffice to say that the above implementation is irrevocably broken.)
|
536
540
|
|
537
541
|
What we would really like is a way to add methods to just one object temporarily, and then remove them, restoring the original state (including any methods we may have overridden when we added ours.) Ruby _almost_ provides a reasonable way to do this, using the +extend+ method. This method lets you add a module's methods to a single object, like this:
|
538
542
|
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
543
|
+
module MyExtension
|
544
|
+
def foo
|
545
|
+
puts "foo called"
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
s1 = 'hello'
|
550
|
+
s2 = 'world'
|
551
|
+
s1.extend(MyExtension) # adds the "foo" method only to object s1,
|
552
|
+
# not to the entire string class.
|
553
|
+
s1.foo # prints "foo called"
|
554
|
+
s2.foo # NameError: s2 is unchanged
|
550
555
|
|
551
556
|
Unfortunately, there is no way to remove the module from the object. Ruby has no "unextend" capability. This omission led Why to implement it himself as a Ruby language extension, lovingly entitled {"mixico"}[http://github.com/why/mixico/tree/master]. The name comes from the library's ability to add and remove "mixins" at will. A similar library exists as a gem called {mixology}[http://www.somethingnimble.com/bliki/mixology]. The two libraries use different APIs but perform the same basic function. For the discussion below, I will assume mixico is installed. However, the library I describe in the next section uses mixology because it is available as a gem.
|
552
557
|
|
553
558
|
Using mixico, we can now write the +draw+ method like this:
|
554
559
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
+
def draw(&block)
|
561
|
+
clear!
|
562
|
+
Module.mix_eval(MapperModule, &block)
|
563
|
+
named_routes.install
|
564
|
+
end
|
560
565
|
|
561
566
|
Wow! That was simple. Mixico even handles all the eval-block-binding crap for us. But the simplicity is a little deceptive: when we want to do a robust implementation, we run into two issues. First, we run into a challenge if we want to support multiple DSL blocks being invoked at once: for example in the case of nested blocks or multithreading. It is possible in such cases that a MapperModule is already mixed into the block's context. The <tt>mix_eval</tt> method by itself, as of this writing, doesn't handle this case well: the inner invocation will remove the module prematurely. Additional logic is necessary to track how many nested invocations (or invocations from other threads) want to mix-in each particular module into each object.
|
562
567
|
|
563
|
-
The other challenge is that of creating the +MapperModule+ module, implementing the +connect+ method and any others we want to mix-in. Because we're adding methods to someone else's object, we need to be as unobtrusive as possible, yet we need to provide the necessary functionality, including invoking the <tt>add_route</tt> method back on the +RouteSet+. This is unfortunately not trivial. I'll describe a full implementation in the next section
|
568
|
+
The other challenge is that of creating the +MapperModule+ module, implementing the +connect+ method and any others we want to mix-in. Because we're adding methods to someone else's object, we need to be as unobtrusive as possible, yet we need to provide the necessary functionality, including invoking the <tt>add_route</tt> method back on the +RouteSet+. This is unfortunately not trivial. I'll describe a full implementation in the next section, but for now let's explore some possible approaches.
|
564
569
|
|
565
570
|
Rails's original +Mapper+ proxy class, we recall from our earlier discussion, used an instance variable, <tt>@set</tt>, which pointed back to the +RouteSet+ instance and thus provided a way to invoke <tt>add_route</tt>. One approach could be to add such an instance variable to the block's context object, so it's available in methods of +MapperModule+. This seems to be the easiest approach, but it is also dangerous because it intrudes on the context object, adding an instance variable and potentially clobbering one used by the caller. Furthermore, in the case of nested blocks that try to add methods to the same object, the two blocks may clobber each other's instance variables.
|
566
571
|
|
567
572
|
Instead of adding information to the block's context object, it may be plausible simply to stash the information away in a global location, such as a class variable, that can be accessed by the +MapperModule+ from within the block. Again, this seems to work, until you have nested or multithreaded usage. It then becomes neccessary to keep a stack of references to handle nesting, and thread-local variables to handle multithreading-- all feasible to do, but a lot of work.
|
568
573
|
|
569
|
-
A third approach
|
574
|
+
A third approach involves dynamically generating a singleton module, "hard coding" a reference to the +RouteSet+ in the module. For example:
|
570
575
|
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
576
|
+
def draw(&block)
|
577
|
+
clear!
|
578
|
+
save_self = self
|
579
|
+
mapper_module = Module.new
|
580
|
+
mapper_module.module_eval do
|
581
|
+
define_method(:connect) do |path,options|
|
582
|
+
save_self.add_route(path,options)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
Module.mix_eval(mapper_module, &block)
|
586
|
+
named_routes.install
|
587
|
+
end
|
583
588
|
|
584
|
-
This probably can be made to work, and it also has the benefit of solving the nesting and multithreading issue neatly since each mixin is done exactly once. However, it seems to be a fairly heavyweight solution: creating a new module for every DSL block invocation may have performance implications. It is also not clear how to support constructs that are not available to <tt>define_method</tt>, such as blocks and parameter default values. However, such an approach may still be useful in cases
|
589
|
+
This probably can be made to work, and it also has the benefit of solving the nesting and multithreading issue neatly since each mixin is done exactly once. However, it seems to be a fairly heavyweight solution: creating a new module for every DSL block invocation may have performance implications. It is also not clear how to support constructs that are not available to <tt>define_method</tt>, such as blocks and parameter default values. However, such an approach may still be useful in certain cases when you need a dynamically generated DSL depending on the context.
|
585
590
|
|
586
591
|
As we have seen, the mixin idea seems like a compelling solution, particularly in conjunction with Gray's arity check, but the implementation details present some challenges. It may be a winner if a library can be written to hide the implementation complexity. Let's summarize this approach, and then proceed to examine such a library, one that uses some of the best of what we've discussed to make implementing DSL blocks simple.
|
587
592
|
|
588
593
|
*Implementation*:
|
589
594
|
|
590
|
-
* Install a mixin library such as mixico or mixology
|
595
|
+
* Install a mixin library such as mixico or mixology.
|
591
596
|
* Define the DSL methods in a module.
|
592
|
-
* Mix the module into the block's context before invoking the block, and remove it afterwards
|
593
|
-
* Carefully handle any issues involving nested blocks
|
597
|
+
* Mix the module into the block's context before invoking the block, and remove it afterwards.
|
598
|
+
* Carefully handle any issues involving nested blocks and multithreading while remaining unobtrusive.
|
594
599
|
|
595
600
|
*Pros*:
|
596
601
|
|
597
|
-
* Allows the concise syntax without a block parameter
|
598
|
-
* Doesn't change +self+, thus preserving the right behavior regarding helper methods and instance variables
|
602
|
+
* Allows the concise syntax without a block parameter.
|
603
|
+
* Doesn't change +self+, thus preserving the right behavior regarding helper methods and instance variables.
|
599
604
|
|
600
605
|
*Cons*:
|
601
606
|
|
602
|
-
* Requires
|
603
|
-
* Implementation is
|
607
|
+
* Requires an extension to Ruby to implement mixin removal.
|
608
|
+
* Implementation is complicated and error-prone.
|
604
609
|
* Does nothing to solve the helper method vs DSL method ambiguity
|
605
610
|
|
606
611
|
*Verdict*: Use it for cases where parameterless blocks are desired, if a library is available to handle the details of the implementation.
|
@@ -609,53 +614,92 @@ As we have seen, the mixin idea seems like a compelling solution, particularly i
|
|
609
614
|
|
610
615
|
As we have seen, the mixin implementation has some compelling qualities, but is hampered by the difficulty of implementing it in a robust way. It could be a useful implementation if a library were present to handle the details.
|
611
616
|
|
612
|
-
Blockenspiel was written to be that library. It provides a comprehensive and robust implementation of the mixin strategy, correctly handling nesting and multithreading. It offers the option to perform an arity check, giving the caller the choice of whether or not to use a block parameter. You can even tell Blockenspiel to use <tt>instance_eval</tt> instead of a mixin, in those cases when it is appropriate. Finally, Blockenspiel provides
|
617
|
+
Blockenspiel was written to be that library. It provides a comprehensive and robust implementation of the mixin strategy, correctly handling nesting and multithreading. It offers the option to perform an arity check, giving the caller the choice of whether or not to use a block parameter. You can even tell Blockenspiel to use <tt>instance_eval</tt> instead of a mixin, in those cases when it is appropriate. Finally, Blockenspiel also provides an API for dynamic construction of DSLs.
|
613
618
|
|
614
|
-
But most importantly, it is easy to use. To write a DSL, just follow the first and easiest implementation strategy, creating a proxy class that can be passed into the block as a parameter. Then instead of yielding the proxy, pass it to Blockenspiel, and it will do the rest.
|
619
|
+
But most importantly, it is easy to use. To write a basic DSL, just follow the first and easiest implementation strategy, creating a proxy class that can be passed into the block as a parameter. Then instead of yielding the proxy object, pass it to Blockenspiel, and it will do the rest.
|
615
620
|
|
616
621
|
Our Rails routing example implemented using Blockenspiel might look like this:
|
617
622
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
623
|
+
class RouteSet
|
624
|
+
|
625
|
+
class Mapper
|
626
|
+
include Blockenspiel::DSL # Tell Blockenspiel this is a DSL proxy
|
627
|
+
|
628
|
+
def initialize(set)
|
629
|
+
@set = set
|
630
|
+
end
|
631
|
+
|
632
|
+
def connect(path, options = {})
|
633
|
+
@set.add_route(path, options)
|
634
|
+
end
|
635
|
+
# ...
|
636
|
+
end
|
637
|
+
|
638
|
+
# ...
|
639
|
+
|
640
|
+
def draw(&block)
|
641
|
+
clear!
|
642
|
+
Blockenspiel.invoke(block, Mapper.new(self)) # Blockenspiel does the rest
|
643
|
+
named_routes.install
|
644
|
+
end
|
645
|
+
|
646
|
+
# ...
|
647
|
+
|
648
|
+
def add_route(path, options = {})
|
649
|
+
# ...
|
645
650
|
|
646
651
|
The code above is as simple as a block parameter or <tt>instance_eval</tt> implementation. However, it performs a full-fledged mixin implementation, and even throws in the arity check. We recall from the previous section that one of the chief challenges is to mediate communication between the mixin and proxy in a re-entrant and thread-safe way. Blockenspiel implements this mediation using a global hash, avoiding the compatibility risk of adding instance variables to the block's context object, and avoiding the performance hit of dynamically generating proxies. All the implementation details are carefully handled behind the scenes.
|
647
652
|
|
648
|
-
Atop this basic usage, Blockenspiel provides two types of customization. First, you can customize the DSL, using a few simple directives to specify which methods on your proxy should be available in the mixin implementation, possibly even under different names.
|
653
|
+
Atop this basic usage, Blockenspiel provides two types of customization. First, you can customize the DSL, using a few simple directives to specify which methods on your proxy should be available in the mixin implementation, possibly even under different names. Method renaming is an important feature of DSL blocks that don't have parameters, because of a syntactic issue with <tt>attr_writer</tt>. Consider, for example, this simple DSL proxy object:
|
654
|
+
|
655
|
+
class ConfigMethods
|
656
|
+
include Blockenspiel::DSL
|
657
|
+
attr_writer :author
|
658
|
+
attr_writer :title
|
659
|
+
end
|
660
|
+
|
661
|
+
You might interact with it in a DSL block that uses parameters, like so:
|
662
|
+
|
663
|
+
create_paper do |config|
|
664
|
+
config.author = "Daniel Azuma"
|
665
|
+
config.title = "Implementing DSL Blocks"
|
666
|
+
end
|
667
|
+
|
668
|
+
However, if you try to use it in a parameter-less DSL block that uses mixins (or <tt>instance_eval</tt> or any other such technique), you run into this dilemma:
|
669
|
+
|
670
|
+
create_paper do
|
671
|
+
author = "Daniel Azuma" # Whoops! These no longer work because they
|
672
|
+
title = "Implementing DSL Blocks" # look like local variable assignments!
|
673
|
+
end
|
674
|
+
|
675
|
+
When you design a DSL for use with parameter-less blocks, you need an alternate API. For example, if you could just rename the methods, the syntax would no longer be ambiguous:
|
676
|
+
|
677
|
+
create_paper do
|
678
|
+
set_author "Daniel Azuma" # These are unambiguously method calls.
|
679
|
+
set_title "Implementing DSL Blocks"
|
680
|
+
end
|
681
|
+
|
682
|
+
Blockenspiel gives you this ability:
|
683
|
+
|
684
|
+
class ConfigMethods
|
685
|
+
include Blockenspiel::DSL
|
686
|
+
attr_writer :author
|
687
|
+
attr_writer :title
|
688
|
+
dsl_method :set_author, :author= # Make the methods available in parameterless
|
689
|
+
dsl_method :set_title, :title= # blocks under these alternate names.
|
690
|
+
end
|
691
|
+
|
692
|
+
Second, you can customize the invocation, specifying whether to perform an arity check, whether to use <tt>instance_eval</tt> instead of mixins, and various other minor behavioral adjustments, by providing parameters to the <tt>Blockenspiel#invoke</tt> method. All the details are handled by the Blockenspiel library, leaving you free to focus on your API.
|
649
693
|
|
650
694
|
Blockenspiel is available as a gem:
|
651
695
|
|
652
|
-
|
696
|
+
gem install blockenspiel
|
653
697
|
|
654
|
-
It requires the mixology gem to handle mixin removal.
|
698
|
+
It currently requires the mixology gem to handle mixin removal.
|
655
699
|
|
656
|
-
===
|
700
|
+
=== Summary
|
657
701
|
|
658
|
-
DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently occurred surrounding the implementation of DSL blocks, particularly addressing the desire to eliminate the need for parameters to the block. We have discussed five different strategies for DSL block implementation, each with its own advantages and disadvantages. Of these, the mixin strategy, recently proposed by Why The Lucky Stiff, appears promising, but its implementation is complex and requires attention to a number of details. The Blockenspiel library provides a concrete and robust implementation of this strategy, hiding the implementation complexity.
|
702
|
+
DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently occurred surrounding the implementation of DSL blocks, particularly addressing the desire to eliminate the need for parameters to the block. We have discussed five different strategies for DSL block implementation, each with its own advantages and disadvantages. Of these, the mixin strategy, recently proposed by Why The Lucky Stiff, appears promising, but its implementation is complex and requires attention to a number of details. The Blockenspiel library provides a concrete and robust implementation of this strategy, hiding the implementation complexity while providing a number of features useful for writing DSL blocks.
|
659
703
|
|
660
704
|
=== References
|
661
705
|
|