blockenspiel 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ === 0.0.4 / 2008-10-24
2
+
3
+ * Improvements to the logic for choosing behaviors
4
+ * Added exception classes and provided better error handling
5
+ * Actually added the behavior test case to the gem manifest...
6
+ * Documentation revisions
7
+ * Revisions to the Implementing DSL Blocks paper
8
+
1
9
  === 0.0.3 / 2008-10-23
2
10
 
3
11
  * Added :proxy behavior for parameterless blocks
@@ -1,8 +1,8 @@
1
1
  == Implementing DSL Blocks
2
2
 
3
- by Daniel Azuma, 19 October 2008
3
+ by Daniel Azuma, 28 October 2008
4
4
 
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.
5
+ A <em>DSL block</em> is a construct commonly used in Ruby APIs. 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
 
@@ -144,7 +144,28 @@ However, some have argued that it is too verbose. Why, in a DSL, is it necessary
144
144
  # etc.
145
145
  end
146
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:
147
+ In the next section we will look at this possible syntax improvement more closely. But first, let us summarize our discussion of the "block parameter" implementation.
148
+
149
+ *Implementation*:
150
+
151
+ * Create a proxy class defining the DSL.
152
+ * Yield the proxy object to the block as a parameter.
153
+
154
+ *Pros*:
155
+
156
+ * Easy to implement.
157
+ * Clear syntax for the caller.
158
+ * Clear separation between the DSL and surrounding code.
159
+
160
+ *Cons*:
161
+
162
+ * Requires a block parameter, sometimes resulting in verbose or clumsy syntax.
163
+
164
+ <b>Use it when</b>: you want a simple, effective DSL block and don't mind requiring a parameter.
165
+
166
+ === Parameterless block syntax
167
+
168
+ Much of the recent discussion surrounding DSL blocks originates from a desire to eliminate the block parameter. A domain-specific _language_, it is reasoned, should be as natural and concise as possible, and should not be tied down to the syntax of method invocation. In many cases, eliminating parameters would have an enormous impact on the readability of a DSL block. One common example is the case of nested blocks, which, because of Ruby 1.8's scoping semantics, require different variable and parameter names. Consider an imaginary DSL block that looks like this:
148
169
 
149
170
  create_container do |container|
150
171
  container.create_subcontainer do |subcontainer1|
@@ -178,28 +199,44 @@ That was clunky. Wouldn't it be nice to instead see this?...
178
199
  end
179
200
  end
180
201
 
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.
202
+ While this is often an improvement, it does come at a cost, and it is important to bear this cost in mind as we delve into implementations of parameterless DSL blocks. First, certain method names become syntactically unavailable when you eliminate the method call syntax. Consider, for example, this simple DSL proxy object that uses <tt>attr_writer</tt>...
182
203
 
183
- *Implementation*:
204
+ class ConfigMethods
205
+ attr_writer :author
206
+ attr_writer :title
207
+ end
184
208
 
185
- * Create a proxy class defining the DSL.
186
- * Yield the proxy object to the block as a parameter.
209
+ You might interact with it in a DSL block that uses parameters, like so:
187
210
 
188
- *Pros*:
211
+ create_paper do |config|
212
+ config.author = "Daniel Azuma"
213
+ config.title = "Implementing DSL Blocks"
214
+ end
189
215
 
190
- * Easy to implement.
191
- * Clear syntax for the caller.
192
- * Clear separation between the DSL and surrounding code.
216
+ However, if you try to eliminate the block parameter, you run into this dilemma:
193
217
 
194
- *Cons*:
218
+ create_paper do
219
+ author = "Daniel Azuma" # Whoops! These no longer work because they
220
+ title = "Implementing DSL Blocks" # look like local variable assignments!
221
+ end
222
+
223
+ You are forced either to explicitly specify, for example, "<tt>self.author=</tt>", or you must provide different names for your DSL methods. Similarly, many operators, notably <tt>[]=</tt>, are syntactically not available unless you use a full method call syntax.
195
224
 
196
- * Verbose: requires a block parameter, sometimes resulting in clumsy syntax.
225
+ Second, and more importantly, by eliminating the block parameter, we eliminate the primary means of distinguishing which methods belong to the DSL, and which methods do not. For example, in our routing example, if we eliminate the parameter, like so:
197
226
 
198
- *Verdict*: Use it if you want a simple, effective DSL block and don't mind requiring a parameter.
227
+ ActionController::Routing::Routes.draw do
228
+ connect ':controller/:action/:id'
229
+ connect ':controller/:action/:page/:format'
230
+ # etc.
231
+ end
232
+
233
+ ...we now _assume_ that the +connect+ method is part of the DSL, but that is no longer explicit in the syntax. If, suppose +connect+ was also a method of whatever object was +self+ in the context of the block, which method should be called? There is a method lookup ambiguity inherent to the syntax itself, and, as we shall see, different implementations of parameterless blocks will resolve this ambiguity in different, and sometimes confusing, ways.
234
+
235
+ Despite the above caveats inherent to the syntax, the desire to eliminate the block parameter is quite strong. Let's consider how it can be done.
199
236
 
200
237
  === Implementation strategy 2: instance_eval
201
238
 
202
- Micah Martin's post[http://blog.8thlight.com/articles/2007/05/20/] describes an alternate implementation strategy that does not require the block to take a parameter. Instead, he suggests using a powerful, if sometimes confusing, Ruby metaprogramming tool called <tt>instance_eval</tt>. This method, defined on the +Object+ class so it is available to every object, has a simple function: it executes a block given it, but does so with the +self+ reference redirected to the receiver. Hence, within the block, calling a method, or accessing an instance variable or class variable, (or, in Ruby 1.9, accessing a constant), will begin at a different place.
239
+ Micah Martin's {blog post}[http://blog.8thlight.com/articles/2007/05/20/] describes an implementation strategy that does not require the block to take a parameter. He suggests using a powerful, if sometimes confusing, Ruby metaprogramming tool called <tt>instance_eval</tt>. This method, defined on the +Object+ class so it is available to every object, has a simple function: it executes a block given it, but does so with the +self+ reference redirected to the receiver. Hence, within the block, calling a method, or accessing an instance variable or class variable, (or, in Ruby 1.9, accessing a constant), will begin at a different place.
203
240
 
204
241
  It is perhaps instructive to see an example. Let's create a simple class
205
242
 
@@ -309,7 +346,7 @@ The problem gets worse. Changing +self+ affects not only how methods are looked
309
346
 
310
347
  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.
311
348
 
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.
349
+ 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 the method lookup ambiguity discussed in the previous section. If +self+ has changed inside the block, and we tried to call +makeurl+, we might expect a +NoMethodError+ to be raised for +makeurl+ on the +Mapper+ class, rather than for "<tt>[]</tt>" on the +Symbol+ class. However, things change when we recall 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. Any name is a valid DSL 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.
313
350
 
314
351
  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.
315
352
 
@@ -333,15 +370,15 @@ So does this mean we're stuck with block parameters for better or worse? Not qui
333
370
  * Surprising lookup behavior for helper methods.
334
371
  * Surprising lookup behavior for instance variables.
335
372
  * Breaks encapuslation of the proxy class.
336
- * Possibility of a helper method vs DSL method ambiguity.
373
+ * Encounters the helper method vs DSL method ambiguity.
337
374
 
338
- *Verdict*: Use it if you are writing a DSL that constructs classes or modifies class internals. Otherwise try to avoid it.
375
+ <b>Use it when</b>: you are writing a DSL that constructs classes or modifies class internals.
339
376
 
340
377
  === Implementation strategy 3: delegation
341
378
 
342
379
  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].
343
380
 
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.
381
+ The basic 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.
345
382
 
346
383
  class MyClass
347
384
  def foo
@@ -389,7 +426,7 @@ Going back to our modification of the Rails routing code, let's see what this lo
389
426
 
390
427
  def draw(&block)
391
428
  clear!
392
- original_self = Kernel.eval('self', block.binding) # Get the block's context self
429
+ original_self = Kernel.eval('self', block.binding) # Get block's context self
393
430
  map = Mapper.new(self, original_self) # Give it to the proxy
394
431
  map.instance_eval(&block)
395
432
  named_routes.install
@@ -400,7 +437,7 @@ Going back to our modification of the Rails routing code, let's see what this lo
400
437
  def add_route(path, options = {})
401
438
  # ...
402
439
 
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:
440
+ 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. We have not solved that ambiguity: 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
441
 
405
442
  URL_PREFIX = 'mywebsite/:controller/:action/'
406
443
  def makeurl(*params)
@@ -413,7 +450,65 @@ Now people familiar with how Rails is implemented will probably object that +Map
413
450
  # etc.
414
451
  end
415
452
 
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.
453
+ While this would appear to have solved the helper method issue, so far 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. By using <tt>instance_eval</tt>, we still break encapsulation of the proxy class.
454
+
455
+ Addressing the instance variable issue is not as straightforward as delegating method calls. There is, as far as I know, no direct way to delegate instance variable lookup, and Manges's blog posting does not attempt to provide a solution either. However, we can imagine a few techniques to mitigate the problem. First, we could eliminate the proxy object's dependence on instance variables altogether, by replacing them with a global hash. In our example, instead of keeping a reference to the +RouteSet+ as an instance variable of +Mapper+, we can maintain a global hash that looks up the +RouteSet+ using the +Mapper+ instance as the key. In this way, we eliminate the risk of the block clobbering the proxy's state, and minimize the problem of breaking encapsulation of the proxy object.
456
+
457
+ Second, we could make instance variables from the block's context partially available through a "pull-push" technique using <tt>instance_variable_set</tt> and <tt>instance_variable_get</tt> calls. Before calling the block, we "pull" in the block context object's instance variables, byt iterating over them and setting the same instance variables on the proxy object. Then those instance variables will appear to be still available during the block. On completing the block, we then "push" any changes back to the block context object, by iterating over the proxy's instance variables and setting them on the block context object.
458
+
459
+ Here is a sample implementation of these two techniques for handling instance variables:
460
+
461
+ class RouteSet
462
+
463
+ class Mapper
464
+
465
+ @@routeset_map = Hash.new # Global hashes to replace
466
+ @@original_self_map = Hash.new # Mapper's instance variables
467
+
468
+ def initialize(set, original_self)
469
+ @@routeset_map[self] = set # Add me to global hashes
470
+ @@original_self_map[self] = original_self
471
+ original_self.instance_variables.each do |name| # "pull" instance variables
472
+ instance_variable_set(name, original_self.instance_variable_get(name))
473
+ end
474
+ end
475
+
476
+ def cleanup
477
+ @@routeset_map.delete(self) # Remove from global hashes
478
+ original_self = @@original_self_map.delete(self)
479
+ instance_variables.each do |name| # "push" instance variables
480
+ original_self.instance_variable_set(name, instance_variable_get(name))
481
+ end
482
+ end
483
+
484
+ def connect(path, options = {})
485
+ @@routeset_map[self].add_route(path, options) # Lookup set from global hash
486
+ end
487
+
488
+ # ...
489
+
490
+ def method_missing(name, *params, &blk) # Lookup original self
491
+ @@original_self_map[self].send(name, *params, &blk) # from global hash
492
+ end
493
+ end
494
+
495
+ # ...
496
+
497
+ def draw(&block)
498
+ clear!
499
+ original_self = Kernel.eval('self', block.binding)
500
+ map = Mapper.new(self, original_self)
501
+ map.instance_eval(&block)
502
+ map.cleanup
503
+ named_routes.install
504
+ end
505
+
506
+ # ...
507
+
508
+ def add_route(path, options = {})
509
+ # ...
510
+
511
+ While these measures seem to handle most of the cases, the implementation is getting more complex, and includes the additional overhead of hash lookups and copying of instance variables. More significantly, the "pull-push" technique does not quite preserve the expected semantics of instance variables. For instance, if you change an instance variable's value inside the block, it will get "pushed" back to the context object after the block is completed, but until then, the context object will not know about the change. So if, in the meantime, you called a helper method that relies on that instance variable, you will get the old value, and this can result in confusion. Using global hashes might be an effective means of protecting the proxy object's internals from the block. However, I find the "pull-push" technique to delegate instance variables to be of questionable value.
417
512
 
418
513
  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
514
 
@@ -428,7 +523,7 @@ Several variations on the delegation theme have been proposed. One such variatio
428
523
 
429
524
  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.
430
525
 
431
- Let's wrap up our discussion of delegation and then delve into some different, and perhaps more useful, ideas.
526
+ Let's wrap up our discussion of delegation and then delve into an entirely different approach.
432
527
 
433
528
  *Implementation*:
434
529
 
@@ -443,12 +538,11 @@ Let's wrap up our discussion of delegation and then delve into some different, a
443
538
 
444
539
  *Cons*:
445
540
 
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.
541
+ * No complete way to eliminate the surprising lookup behavior for instance variables.
542
+ * Does not solve the helper method vs DSL method ambiguity.
449
543
  * Harder to implement than a simple <tt>instance_eval</tt>.
450
544
 
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 try to avoid it.
545
+ <b>Use it when</b>: you have a case where <tt>instance_eval</tt> is appropriate (i.e. if you are writing a DSL that constructs classes or modifies class internals) but you are worried about helper methods being available.
452
546
 
453
547
  === Implementation strategy 4: arity detection
454
548
 
@@ -501,8 +595,8 @@ Let us summarize Gray's arity detection technique, and then proceed to an intere
501
595
 
502
596
  *Pros*:
503
597
 
504
- * Best of both worlds.
505
- * Puts the choice in the hands of the caller, who can best make the decision.
598
+ * Gives the caller the ability to choose which syntax works best.
599
+ * Solves method lookup ambiguity.
506
600
  * Implementation cost is not significant.
507
601
 
508
602
  *Cons*:
@@ -510,7 +604,7 @@ Let us summarize Gray's arity detection technique, and then proceed to an intere
510
604
  * Not an all-encompassing solution-- either choice still has its own pros and cons.
511
605
  * Possibility of dilution of DSL branding.
512
606
 
513
- *Verdict*: Use it for most cases when it is not clear whether block parameters or <tt>instance_eval</tt> is better.
607
+ <b>Use it when</b>: it is not clear whether block parameters or <tt>instance_eval</tt> is better, or if you need a way to mitigate the method lookup ambiguity.
514
608
 
515
609
  === Implementation strategy 5: mixins
516
610
 
@@ -563,13 +657,13 @@ Using mixico, we can now write the +draw+ method like this:
563
657
  named_routes.install
564
658
  end
565
659
 
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.
660
+ Wow! That was simple. Mixico even handles all the eval-block-binding hackery 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.
567
661
 
568
662
  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.
569
663
 
570
664
  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.
571
665
 
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.
666
+ 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. This is of course the same strategy we used to eliminate instance variables in the section on delegation. 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.
573
667
 
574
668
  A third approach involves dynamically generating a singleton module, "hard coding" a reference to the +RouteSet+ in the module. For example:
575
669
 
@@ -588,7 +682,7 @@ A third approach involves dynamically generating a singleton module, "hard codin
588
682
 
589
683
  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.
590
684
 
591
- One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there is an ambiguity regarding whether methods should be directed to the DSL or to the surrounding context. In the implementations we've discussed previously, based on <tt>instance_eval</tt>, the actual behavior is fairly straightforward to reason about. A simple <tt>instance_eval</tt> disables method calls to the block's context altogether: you can call _only_ the DSL methods. An <tt>instance_eval</tt> with delegation re-enables method calls to the block's context but gives the DSL priority. If both the DSL and the surrounding block define the same method name, the DSL's method will be called.
685
+ One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there remains an ambiguity regarding whether methods should be directed to the DSL or to the surrounding context. In the implementations we've discussed previously, based on <tt>instance_eval</tt>, the actual behavior is fairly straightforward to reason about. A simple <tt>instance_eval</tt> disables method calls to the block's context altogether: you can call _only_ the DSL methods. An <tt>instance_eval</tt> with delegation re-enables method calls to the block's context but gives the DSL priority. If both the DSL and the surrounding block define the same method name, the DSL's method will be called.
592
686
 
593
687
  Mixin's behavior is less straightforward, because of a subtlety in Ruby's method lookup behavior. Under most cases, it behaves similarly to an <tt>instance_eval</tt> with delegation: the DSL's methods take priority. However, if methods have been added directly to the object, they will take precedence over the DSL's methods. Following is an example of this case:
594
688
 
@@ -658,13 +752,13 @@ As we have seen, the mixin idea seems like it may be a compelling solution, part
658
752
  * Implementation is complicated and error-prone.
659
753
  * The helper method vs DSL method ambiguity remains, exhibiting surprising behavior in the presence of singleton methods.
660
754
 
661
- *Verdict*: Use it for cases where parameterless blocks are desired and the method lookup ambiguity can be mitigated, as long as a library is available to handle the details of the implementation.
755
+ <b>Use it when</b>: parameterless blocks are desired and the method lookup ambiguity can be mitigated, as long as a library is available to handle the details of the implementation.
662
756
 
663
757
  === Blockenspiel: a comprehensive implementation
664
758
 
665
- 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.
759
+ Some of the implementations we have covered, especially the mixin implementation, have some compelling qualities, but are hampered by the difficulty of implementing them in a robust way. It could be a useful implementation if a library were present to handle the details.
666
760
 
667
- 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.
761
+ Blockenspiel was written to be that library. It first 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 an alternate implementation, such as <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.
668
762
 
669
763
  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.
670
764
 
@@ -700,56 +794,90 @@ Our Rails routing example implemented using Blockenspiel might look like this:
700
794
 
701
795
  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.
702
796
 
703
- 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:
797
+ 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. You can also cause methods to be available in the mixin under different names, thus sidestepping the <tt>attr_writer</tt> issue we discussed earlier. If you want methods of the form "attribute=" on your proxy object, Blockenspiel provides a simple syntax for renaming them:
704
798
 
705
799
  class ConfigMethods
706
800
  include Blockenspiel::DSL
707
801
  attr_writer :author
708
802
  attr_writer :title
803
+ dsl_method :set_author, :author= # Make the methods available in parameterless
804
+ dsl_method :set_title, :title= # blocks under these alternate names.
709
805
  end
710
806
 
711
- You might interact with it in a DSL block that uses parameters, like so:
807
+ Now, when we use block parameters, we use the methods of the original ConfigMethods class:
712
808
 
713
809
  create_paper do |config|
714
810
  config.author = "Daniel Azuma"
715
811
  config.title = "Implementing DSL Blocks"
716
812
  end
717
813
 
718
- 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:
814
+ And omitting the parameter, the alternate method names are mixed in:
719
815
 
720
816
  create_paper do
721
- author = "Daniel Azuma" # Whoops! These no longer work because they
722
- title = "Implementing DSL Blocks" # look like local variable assignments!
817
+ set_author "Daniel Azuma"
818
+ set_title "Implementing DSL Blocks"
723
819
  end
724
820
 
725
- 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:
821
+ Second, you can customize the invocation-- for example specifying whether to perform an arity check, whether to use <tt>instance_eval</tt> instead of mixins, and various other minor behavioral adjustments-- simply by providing parameters to the <tt>Blockenspiel#invoke</tt> method. All the implementation details are handled by the Blockenspiel library, leaving you free to focus on your API.
726
822
 
727
- create_paper do
728
- set_author "Daniel Azuma" # These are unambiguously method calls.
729
- set_title "Implementing DSL Blocks"
823
+ Third, Blockenspiel provides an API, itself a DSL block, letting you dynamically construct DSLs. Suppose, for the sake of argument, we wanted to let the caller optionally rename the +connect+ method, for example because we want to make the name "connect" available for named routes. That is, suppose we wanted to provide this behavior:
824
+
825
+ ActionController::Routing::Routes.draw(:method => :myconnect) do |map|
826
+ map.myconnect ':controller/:action/:id'
827
+ map.myconnect ':controller/:action/:page/:format'
828
+ # etc.
730
829
  end
731
830
 
732
- Blockenspiel gives you this ability:
831
+ This can be implemented by using Blockenspiel to dynamically generate the proxy class, as follows:
733
832
 
734
- class ConfigMethods
735
- include Blockenspiel::DSL
736
- attr_writer :author
737
- attr_writer :title
738
- dsl_method :set_author, :author= # Make the methods available in parameterless
739
- dsl_method :set_title, :title= # blocks under these alternate names.
740
- end
833
+ class RouteSet
834
+
835
+ # We don't define a Mapper class anymore!
836
+
837
+ def draw(options={}, &block)
838
+ clear!
839
+ method_name = options[:method] || :connect # The method name for the DSL to use
840
+ save_self = self # Save a reference to the RouteSet
841
+ Blockenspiel.invoke(block) do # Dynamically create a "mapper" object
842
+ add_method(method_name) do |path, *args| # Dynamically add the method
843
+ save_self.add_route(path, *args) # Call back to the RouteSet
844
+ end
845
+ end
846
+ named_routes.install
847
+ end
848
+
849
+ # ...
850
+
851
+ def add_route(path, options = {})
852
+ # ...
741
853
 
742
- 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.
854
+ The observant reader will notice two features of the above code. First, Blockenspiel's API for dynamically generating a DSL, itself uses a DSL block, and indeed Blockenspiel uses itself to implement this feature. The <tt>add_method</tt> call is part of Blockenspiel's DSL-generation DSL. And second, this code bears a remarkable resemblance to one of the approaches to implementing the proxy class in our discussion on mixins. Indeed, this is Blockenspiel's way of supporting that implementation strategy.
743
855
 
744
- Blockenspiel is available as a gem:
856
+ Blockenspiel is available now as a gem for MRI 1.8.x
745
857
 
746
858
  gem install blockenspiel
747
859
 
748
- It currently requires the mixology gem to handle mixin removal.
860
+ More information is available on Blockenspiel's Rubyforge page at http://virtuoso.rubyforge.org/blockenspiel
861
+
862
+ Source code is available on Github at http://github.com/dazuma/blockenspiel
749
863
 
750
864
  === Summary
751
865
 
752
- 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.
866
+ DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently surrounded the implementation of DSL blocks, particularly addressing the desire to eliminate the need for parameters to the block. We have discussed several different strategies for DSL block implementation, each with its own advantages and disadvantages.
867
+
868
+ The simplest strategy, creating a proxy object and passing a reference to the block as a parameter, is straightforward, safe, and widely used. However, sometimes we might want to provide a cleaner API by eliminating the block parameter.
869
+
870
+ Parameterless blocks inherently pose some syntactic issues. First, it may be ambiguous whether a method is meant to be directed to the DSL or to the block's surrounding context. Second, certain constructions, such as those created by <tt>attr_writer</tt>, are syntactically not allowed and must be renamed.
871
+
872
+ The simplest way to eliminate the block parameter is to change +self+ inside the block using <tt>instance_eval</tt>. This has the side effects of opening the implementation of the proxy object, and cutting off access to the context's helper methods and instance variables.
873
+
874
+ It is possible to mitigate these side effects by delegating methods, and partially delegating instance variables, back to the context object. These are not foolproof mechanisms and are subject to a few cases of surprising behavior.
875
+
876
+ The mixin strategy takes a different approach to parameterless blocks by temporarily "mixing" the DSL methods into the context object itself. This eliminates the side effects of changing the +self+ reference, but requires a more complex implementation, and somewhat exacerbates the method lookup ambiguity.
877
+
878
+ Since the question of whether or not to take a block parameter may be best answered by the caller, it is often useful for an implementation to check the block's arity to determine whether to use a block parameter or a parameterless implementation. However, it is possible for this step to lead to dilution of the DSL's branding.
879
+
880
+ The Blockenspiel library provides a concrete and robust implementation of DSL blocks, based on the best of these ideas. It hides the implementation complexity while providing a number of features useful for writing DSL blocks.
753
881
 
754
882
  === References
755
883
 
@@ -778,3 +906,7 @@ DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flur
778
906
  {Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Mixico}[http://github.com/why/mixico/tree/master]</em> (Ruby library), 2008.
779
907
 
780
908
  {Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Mixing Our Way Out Of Instance Eval?}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]</em>, 2008.10.06.
909
+
910
+ === About the author
911
+
912
+ Daniel Azuma is Chief Software Architect at Zoodango. He has been working with Ruby for about three years, and finds the language generally pleasant to work with, though he thinks the scoping rules could use some improvement. His home page is at http://www.daniel-azuma.com/.
data/Manifest.txt CHANGED
@@ -5,5 +5,6 @@ README.txt
5
5
  Rakefile
6
6
  lib/blockenspiel.rb
7
7
  tests/tc_basic.rb
8
- tests/tc_mixins.rb
8
+ tests/tc_behaviors.rb
9
9
  tests/tc_dsl_methods.rb
10
+ tests/tc_mixins.rb
data/lib/blockenspiel.rb CHANGED
@@ -46,7 +46,28 @@ require 'mixology'
46
46
  module Blockenspiel
47
47
 
48
48
  # Current gem version
49
- VERSION_STRING = '0.0.3'
49
+ VERSION_STRING = '0.0.4'
50
+
51
+
52
+ # Base exception for all exceptions raised by Blockenspiel
53
+
54
+ class BlockenspielError < RuntimeError
55
+ end
56
+
57
+
58
+ # This exception is rasied when attempting to use the <tt>:proxy</tt> or
59
+ # <tt>:mixin</tt> parameterless behavior with a target that does not have
60
+ # the DSL module included. It is an error made by the DSL implementor.
61
+
62
+ class DSLMissingError < BlockenspielError
63
+ end
64
+
65
+
66
+ # This exception is raised when the block provided does not take the
67
+ # expected number of parameters. It is an error made by the caller.
68
+
69
+ class BlockParameterError < BlockenspielError
70
+ end
50
71
 
51
72
 
52
73
  # === DSL setup methods
@@ -365,8 +386,8 @@ module Blockenspiel
365
386
  # If set to false, disables parameterless blocks and always attempts to
366
387
  # pass a parameter to the block. Otherwise, you may set it to one of
367
388
  # three behaviors for parameterless blocks: <tt>:mixin</tt> (the
368
- # default), <tt>:mixin_inheriting</tt>, and <tt>:instance</tt>. See
369
- # below for a description of these behaviors.
389
+ # default), <tt>:instance</tt>, and <tt>:proxy</tt>. See below for
390
+ # detailed descriptions of these behaviors.
370
391
  # <tt>:parameter</tt>::
371
392
  # If set to false, disables blocks with parameters, and always attempts
372
393
  # to use parameterless blocks. Default is true, enabling parameter mode.
@@ -376,16 +397,17 @@ module Blockenspiel
376
397
  #
377
398
  # <tt>:mixin</tt>::
378
399
  # This is the default behavior. DSL methods from the target are
379
- # temporarily overlayed on the caller's self object, but self is itself
380
- # not modified, so the helper methods and instance variables from the
381
- # caller's closure remain available. The DSL methods are removed when
382
- # the block completes.
400
+ # temporarily overlayed on the caller's +self+ object, but +self+ still
401
+ # points to the same object, so the helper methods and instance
402
+ # variables from the caller's closure remain available. The DSL methods
403
+ # are removed when the block completes.
383
404
  # <tt>:instance</tt>::
384
405
  # This behavior actually changes +self+ to the target object using
385
406
  # <tt>instance_eval</tt>. Thus, the caller loses access to its own
386
407
  # helper methods and instance variables, and instead gains access to the
387
- # target object's instance variables. Any DSL method changes applied
388
- # using <tt>dsl_method</tt> directives are ignored.
408
+ # target object's instance variables. The target object's methods are
409
+ # not modified: this behavior does not apply any DSL method changes
410
+ # specified using <tt>dsl_method</tt> directives.
389
411
  # <tt>:proxy</tt>::
390
412
  # This behavior changes +self+ to a proxy object created by applying the
391
413
  # DSL methods to an empty object, whose <tt>method_missing</tt> points
@@ -448,10 +470,18 @@ module Blockenspiel
448
470
 
449
471
  def self.invoke(block_, target_=nil, opts_={}, &builder_block_)
450
472
 
451
- # Handle this case gracefully
452
- return nil unless block_
473
+ parameter_ = opts_[:parameter]
474
+ parameterless_ = opts_[:parameterless]
475
+
476
+ # Handle no-target behavior
477
+ if parameter_ == false && parameterless_ == false
478
+ if block_.arity != 0 && block_.arity != -1
479
+ raise Blockenspiel::BlockParameterError, "Block should not take parameters"
480
+ end
481
+ return block_.call
482
+ end
453
483
 
454
- # Handle dynamic target generation
484
+ # Perform dynamic target generation if requested
455
485
  if builder_block_
456
486
  opts_ = target_ || opts_
457
487
  builder_ = Blockenspiel::Builder.new
@@ -459,115 +489,108 @@ module Blockenspiel
459
489
  target_ = builder_._create_target
460
490
  end
461
491
 
462
- # Attempt parameterless block
463
- parameterless_ = opts_[:parameterless]
464
- if parameterless_ != false && (block_.arity == 0 || block_.arity == -1)
465
- if parameterless_ == :instance
492
+ # Handle parametered block case
493
+ if parameter_ != false && (block_.arity == 1 || block_.arity == -2) || parameterless_ == false
494
+ if block_.arity != 1 && block_.arity != -1 && block_.arity != -2
495
+ raise Blockenspiel::BlockParameterError, "Block should take exactly one parameter"
496
+ end
497
+ return block_.call(target_)
498
+ end
499
+
500
+ # Check arity for parameterless case
501
+ if block_.arity != 0 && block_.arity != -1
502
+ raise Blockenspiel::BlockParameterError, "Block should not take parameters"
503
+ end
504
+
505
+ # Handle instance-eval behavior
506
+ if parameterless_ == :instance
507
+ return target_.instance_eval(&block_)
508
+ end
509
+
510
+ # Get the module of dsl methods
511
+ mod_ = target_.class._get_blockenspiel_module rescue nil
512
+ unless mod_
513
+ raise Blockenspiel::DSLMissingError
514
+ end
515
+
516
+ # Get the block's calling context object
517
+ object_ = Kernel.eval('self', block_.binding)
518
+
519
+ # Handle proxy behavior
520
+ if parameterless_ == :proxy
521
+
522
+ # Create proxy object
523
+ proxy_ = Blockenspiel::ProxyDelegator.new
524
+ proxy_.extend(mod_)
525
+
526
+ # Store the target and proxy object so dispatchers can get them
527
+ proxy_delegator_key_ = proxy_.object_id
528
+ target_stack_key_ = [Thread.current.object_id, proxy_.object_id]
529
+ @_proxy_delegators[proxy_delegator_key_] = object_
530
+ @_target_stacks[target_stack_key_] = [target_]
531
+
532
+ begin
466
533
 
467
- # Instance-eval behavior.
468
- return target_.instance_eval(&block_)
534
+ # Call the block with the proxy as self
535
+ return proxy_.instance_eval(&block_)
469
536
 
470
- else
537
+ ensure
471
538
 
472
- # Remaining behaviors use the module of dsl methods
473
- mod_ = target_.class._get_blockenspiel_module rescue nil
474
- if mod_
475
-
476
- # Get the block's calling context object
477
- object_ = Kernel.eval('self', block_.binding)
478
-
479
- if parameterless_ == :proxy
480
-
481
- # Proxy behavior:
482
- # Create proxy object
483
- proxy_ = ProxyDelegator.new
484
- proxy_.extend(mod_)
485
-
486
- # Store the target and proxy object so dispatchers can get them
487
- proxy_delegator_key_ = proxy_.object_id
488
- target_stack_key_ = [Thread.current.object_id, proxy_.object_id]
489
- @_proxy_delegators[proxy_delegator_key_] = object_
490
- @_target_stacks[target_stack_key_] = [target_]
491
-
492
- begin
493
-
494
- # Call the block with the proxy as self
495
- return proxy_.instance_eval(&block_)
496
-
497
- ensure
498
-
499
- # Clean up the dispatcher information
500
- @_proxy_delegators.delete(proxy_delegator_key_)
501
- @_target_stacks.delete(target_stack_key_)
502
-
503
- end
504
-
505
- else
506
-
507
- # Mixin behavior:
508
- # Create hash keys
509
- mixin_count_key_ = [object_.object_id, mod_.object_id]
510
- target_stack_key_ = [Thread.current.object_id, object_.object_id]
511
-
512
- # Store the target for inheriting.
513
- # We maintain a target call stack per thread.
514
- target_stack_ = @_target_stacks[target_stack_key_] ||= Array.new
515
- target_stack_.push(target_)
516
-
517
- # Mix this module into the object, if required.
518
- # This ensures that we keep track of the number of requests to
519
- # mix this module in, from nested blocks and possibly multiple threads.
520
- @_mutex.synchronize do
521
- count_ = @_mixin_counts[mixin_count_key_]
522
- if count_
523
- @_mixin_counts[mixin_count_key_] = count_ + 1
524
- else
525
- @_mixin_counts[mixin_count_key_] = 1
526
- object_.mixin(mod_)
527
- end
528
- end
529
-
530
- begin
531
-
532
- # Now call the block
533
- return block_.call
534
-
535
- ensure
536
-
537
- # Clean up the target stack
538
- target_stack_.pop
539
- @_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
540
-
541
- # Remove the mixin from the object, if required.
542
- @_mutex.synchronize do
543
- count_ = @_mixin_counts[mixin_count_key_]
544
- if count_ == 1
545
- @_mixin_counts.delete(mixin_count_key_)
546
- object_.unmix(mod_)
547
- else
548
- @_mixin_counts[mixin_count_key_] = count_ - 1
549
- end
550
- end
551
-
552
- end
553
- # End mixin behavior
554
-
555
- end
556
-
557
- end
558
- # End use of dsl methods module
539
+ # Clean up the dispatcher information
540
+ @_proxy_delegators.delete(proxy_delegator_key_)
541
+ @_target_stacks.delete(target_stack_key_)
559
542
 
560
543
  end
544
+
561
545
  end
562
- # End attempt of parameterless block
563
-
564
- # Attempt parametered block
565
- if opts_[:parameter] != false && block_.arity != 0
566
- return block_.call(target_)
546
+
547
+ # Handle mixin behavior (default)
548
+
549
+ # Create hash keys
550
+ mixin_count_key_ = [object_.object_id, mod_.object_id]
551
+ target_stack_key_ = [Thread.current.object_id, object_.object_id]
552
+
553
+ # Store the target for inheriting.
554
+ # We maintain a target call stack per thread.
555
+ target_stack_ = @_target_stacks[target_stack_key_] ||= Array.new
556
+ target_stack_.push(target_)
557
+
558
+ # Mix this module into the object, if required.
559
+ # This ensures that we keep track of the number of requests to
560
+ # mix this module in, from nested blocks and possibly multiple threads.
561
+ @_mutex.synchronize do
562
+ count_ = @_mixin_counts[mixin_count_key_]
563
+ if count_
564
+ @_mixin_counts[mixin_count_key_] = count_ + 1
565
+ else
566
+ @_mixin_counts[mixin_count_key_] = 1
567
+ object_.mixin(mod_)
568
+ end
567
569
  end
568
570
 
569
- # Last resort fall-back
570
- return block_.call
571
+ begin
572
+
573
+ # Now call the block
574
+ return block_.call
575
+
576
+ ensure
577
+
578
+ # Clean up the target stack
579
+ target_stack_.pop
580
+ @_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
581
+
582
+ # Remove the mixin from the object, if required.
583
+ @_mutex.synchronize do
584
+ count_ = @_mixin_counts[mixin_count_key_]
585
+ if count_ == 1
586
+ @_mixin_counts.delete(mixin_count_key_)
587
+ object_.unmix(mod_)
588
+ else
589
+ @_mixin_counts[mixin_count_key_] = count_ - 1
590
+ end
591
+ end
592
+
593
+ end
571
594
 
572
595
  end
573
596
 
@@ -579,7 +602,7 @@ module Blockenspiel
579
602
 
580
603
  def self._target_dispatch(object_, name_, params_, block_) # :nodoc:
581
604
  target_stack_ = @_target_stacks[[Thread.current.object_id, object_.object_id]]
582
- return TARGET_MISMATCH unless target_stack_
605
+ return Blockenspiel::TARGET_MISMATCH unless target_stack_
583
606
  target_stack_.reverse_each do |target_|
584
607
  target_class_ = target_.class
585
608
  delegate_ = target_class_._get_blockenspiel_delegate(name_)
@@ -587,7 +610,7 @@ module Blockenspiel
587
610
  return target_.send(delegate_, *params_, &block_)
588
611
  end
589
612
  end
590
- return TARGET_MISMATCH
613
+ return Blockenspiel::TARGET_MISMATCH
591
614
  end
592
615
 
593
616
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blockenspiel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-10-23 00:00:00 -07:00
12
+ date: 2008-10-24 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -52,8 +52,9 @@ files:
52
52
  - Rakefile
53
53
  - lib/blockenspiel.rb
54
54
  - tests/tc_basic.rb
55
- - tests/tc_mixins.rb
55
+ - tests/tc_behaviors.rb
56
56
  - tests/tc_dsl_methods.rb
57
+ - tests/tc_mixins.rb
57
58
  has_rdoc: true
58
59
  homepage: http://virtuoso.rubyforge.org/blockenspiel
59
60
  post_install_message: