Saturday, April 29, 2006
Twisting and Shaping DSLs using Ruby Metaprogramming
[..] while reading your article I was thinking about using the recipe name string as an object to route the messages coming from the ingredients and then catching them by implementing missing_method in the string object...
So, for:
recipe "italian breakfast" do
caffe = 1, :macchiato
end
is there a way to translate it into:
"italian breakfast".caffe(1, :macchiato) ?
[..]
Let's see:
"italian breakfast".caffe 1, :macchiato
First of all, I removed the brackets because I don't believe in brackets :-)
What Federico proposes can be done, but I am not sure I would do it this way, unless I am working only within the context of a DSL. If you want your recipe-string to accept the caffe method, then you have to override the method_missing method of the String class, which can be a dangerous thing to do if you have loaded other meta-code that overrides this method on a String or Object level.
I would rather go for a Concept Wrap Up:
Recipe("italian breakfast").caffe 1, :macchiato
where I make explicit the meaning of 'italian breakfast' by wrapping it into a Recipe object. Note that I did not use Recipe. new. I am launching a campaign against the use of new in application code, which I consider at the same level of badly used hungarian notation (because there is such a thing as good hungarian notation).
Another way I would do it is by using what I call a Conjunction:
"italian breakfast".consists_of.caffe 1, :macchiato
You will have to inject a consists_of method in the String class, which will act as a conjunction between the String and the method call. The conjunction will take the String and wrap it up in a Recipe object. The cafe method is then invoked on the Recipe object.
In both cases we have moved the handling of the method_missing from String to a custom made Recipe class.
The conjunction will be:
class String
#Recipe conjuction
def consists_of
Recipe .new self
end
end
The Recipe will start as:
class Recipe
def initialize recipe_name
@name = recipe_name
@ingredients = {}
register_with_global_repository
end
def method_missing method, *args
@ingredients[method .to_s] = args
end
...
The method_missing is catching the ingredients and it stacks them nicely on an hashtable. Why do we need the register_with_global_repository call, though?
Since we are working with a DSL and we are not explicitly storing a reference to a created recipes in a variable, then we get the Recipe to self-register itself with a global centralized repository. I could have made it a @@all_recipes class attribute, but I think that in this simple example it looks clearer as an external separate entity:
$RECIPES_REPOSITORY = {}
and finally the code to print out the recipe:class Recipe
…
private
def register_with_global_repository
$RECIPES_REPOSITORY[ @name] = self
end
end
class Recipe
…
def report
puts "to make #{@name} you should buy:"
@ingredients.each_pair do
|ingredient,description|
puts " * #{Array(description).join ' '} #{ingredient}"
end
end
…
end
What is still missing is a method to print out the recipe directly from the DSL. We would like our call to look like this:
recipe? "italian breakfast"
This snippet fully satisfies our language choice:
Our new DSL now allows us to write:def recipe? recipe_name
$RECIPES_REPOSITORY[recipe_name].report
end
and to obtain:"italian breakfast".consists_of.caffe 1, :macchiato
recipe? "italian breakfast"
to make italian breakfast you should buy:
* 1 macchiato caffe
The one big limitation is that at the moment you can define recipes with only one ingredient.
We get around this limitation by making the method_missing a Follow Up Method Chain:
def allow_method_chaining() self end
The trick here is that after having defined an ingredients we still have the original Recipe object ready to accept new ingredients.def method_missing method, *args
@ingredients [method.to_s] = args
allow_method_chaining
end
You can now do:
"italian breakfast".consists_of.
caffe(1 , :macchiato ).
cornetto(1, :cioccolato)
recipe? "italian breakfast"
and get:
to make italian breakfast you should buy:
* 1 cioccolato cornetto
* 1 macchiato caffe
That was long! I think I will now go for a 1 cioccolato cornetto!
(brackets, fur to non-meaningfulness of do..end in this case)
shopping_list_for "BLT sandwiches" {
three of sliced_bread
two of lettuce
six of tomatoes
four of bacon
}
This requires you to turn down $VERBOSE or you'll get warnings.
Obviously you can use add #of to Fixnum to sidestep the issue, or you can remove it completely.
Eliminating semicolons is easy once you evaluate the block in a context where method_missing is just returning the called symbol.
so.. three, two and so on are implicit methods that get caught by method_missing. 'of' is a nil variable that doen't get used.. or did you miss some "." when copying the code here?
or is it supposed to be three(of(sliced_bread()) with a cascade of method_missings being called?
Can you give us a sample implementation?
"italian breakfast".consists_of.
caffe (1 , :macchiato ).
cornetto (1, :cioccolato)
...spoil most most of the good work before it. I'd probably go for
the usual instance_eval trick with something like:
"italian breakfast".consists_of {
caffe 1, :macchiato
cornetto 1, :cioccolato
}
recipe? "italian breakfast"
(Sample mess, I mean, implementation at the bottom.)
Also when playing with method missing you might want to start with a blank slate, someone might always decide to #freeze a cornetto or do
something we wouldn't expect with a #hash. :-)
require 'singleton'
class RecipeCollection
include Singleton
def self.<<(recipe)
instance << recipe
end
def self.lookup(title)
instance.lookup(title)
end
def <<(recipe)
@recipes ||= []
@recipes << recipe
end
def lookup(title)
@recipes.find { |r| r.title == title }
end
end
class Recipe
def initialize(title, &ingredients_desc)
@title, @ingredients = title, {}
instance_eval(&ingredients_desc)
RecipeCollection << self
end
attr_reader :title
def method_missing(id, *args)
@ingredients[id.to_s] = args
end
def to_s
@title + ":\n" +
@ingredients.collect do |name, description|
description.join(' ') + ' ' + name
end.join("\n")
end
end
class String
def consists_of(&ingredients_desc)
Recipe.new(self, &ingredients_desc)
end
end
def recipe?(name)
puts RecipeCollection.lookup(name)
end
i.e.
Recipe("italian breakfast").caffe 1, :macchiato
i.e.
Recipe("italian breakfast").caffe 1, :macchiato
Killing the New
###############
def Recipe *args
Recipe.new *args
end
now you can just do Recipe(..) and you get a recipe. I call this a Concept Declarator.
In a future post I'll show how to do it semi-autmatically with metaprogramming.
BlankSlate is cool, it removes most methods from a class to give free rein to method_missing users. You are right, it is appropriate to use that too.
Nise site!
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
Good
bad credit auto refinancing
bad credit car refinancing
bad credit home refinancing
bad credit mortgage refinancing
bad credit refinancing
bad credit refinancing home loan
credit mortgage poor refinancing
poor credit refinancing
refinancing home with poor credit
bad credit mortgage refinancing
refinance
home mortgage refinance
refinance loan
auto refinance
refinance mortgage loan
bad credit refinance
refinance rate
home mortgage refinance loan
refinance mortgage rate
california refinance
bad credit mortgage refinance
texas mortgage refinance
refinance second mortgage
florida refinance mortgage
mortgage refinance california
home refinance rate
refinance mortgage interest rate
in refinance
florida refinance
refinance home mortgage rate
business refinance
home loan mortgage refinance loan
auto loan refinance
interest only refinance
refinance interest rate
no closing cost refinance
michigan mortgage refinance
lowest refinance rate
california mortgage refinance loan
refinance house
florida home refinance
refinance home mortgage interest rate
refinance student loan
california refinance home mortgage
bad credit refinance loan
refinance home mortgage home equity loan
refinance san diego
va refinance
refinance 2nd mortgage
mortgage refinance online
refinance comparison
va loan refinance
best refinance mortgage rate
california refinance loan
low refinance rate
poor credit refinance
florida refinance mortgage rate
debt consolidation refinance
cash out refinance
refinance loan rate
home equity mortgage refinance loan
ohio mortgage refinance
refinance after bankruptcy
best refinance mortgage
california mortgage rate refinance
home refinance bad credit
life refinance suv
refinance bankruptcy
houston refinance
mortgage loan refinance florida
california home loan refinance
refinance home equity
bad credit home loan refinance
refinance mortgage new jersey
mortgage refinance lowest rate
refinance mobile home
no credit refinance small business loan
refinance mortgage application
refinance jacksonville
current mortgage refinance rate
miami refinance
mortgage refinance calculator
loan mortgage rate refinance
va home loan refinance
michigan refinance
no cost refinance
mortgage refinance company
mortgage loan refinance and debt consolidation
auto lease refinance
home refinance california
refinance mortgage quote
refinance quote
refinance orlando
small business refinance
refinance company
michigan refinance mortgage loan
norfolk refinance
foreclosure refinance
refinance lender
new york refinance
home loan refinance rate
refinance honolulu
best refinance
business debt refinance
countrywide home loan refinance
refinance dallas
best refinance rate
free quote on refinance
mortgage refinance information
Links to this post:
<< Home

