Flatten not fatten
If you don’t already know, Ruby arrays have a method called flatten. It takes the nested arrays and returns a new one, flattened to a single dimension. Or so says the method description. I like to think of it as a method that takes a pile of stuff and does the right thing.
In Buildr we use arrays extensively, for example, to set up the classpath dependencies (Buildr is for Java, so bear with me here). For example:
compile.with LOG4J, OPENJPA, JAVAX_PERSISTENCE, XERCES, WOODSTOX
Now, what if I already had all these dependencies rolled up in a single array? The I could do this:
compile.with *DEPENDENCIES
The leading star expands the array back into a list of arguments. Which is nice when you first learn about it, but like any leading star, there’s only place for one. If I had a bunch of arrays with dependencies, I could end up with this obfuscated piece of code:
compile.with *([LOG4J] + PERSISTENCE + XML)
Ugly. MySpace ugly, but that’s just my personal sensibility talking.
So we made compile.with take the arguments and flatten them up. Let’s try again:
compile.with LOG4J, PERSISTENCE, XML
Much. Better. As a general convention, you’ll see something like this all over the code:
def with(*specs) @classpath |= Buildr.artifacts(specs.flatten).uniq self end
Why self? See below.
Using :symbol
Some tasks have various options you can set on them. It’s just a matter of getting the options hash and setting the different keys:
compile.options.target = "1.5" compile.options.lint = true
Not too difficult, but you can see from just these two options that there’s a place for some optimization. The kind that makes you think “cool!”, or just snicker “too smart”. We adopted a simple convention, a using method that takes a hash and returns self:
compile.using :target=>"1.5", :lint=>true
Much. Nicer.
So why self? So you can do this:
compile.from("srcs").using(:target=>"1.5").with(LOGGING, PERSISTENCE, XML)
In a different place, we use options to determine which test framework to use. Right now our Chinese menu consists of JUnit and TestNG. So let’s pick a framework:
test.using :framework=>:testng
I think this reads much better than setting an option, but not half as fun as writing this:
test.using :testng
Ok, we’re being way too smart here, but you have to admit there’s some kind of elegance to it. At least, that’s my justification. The using method just gobbles up all the symbols and turns them into true values:
def using(*args)
args.pop.each { |key, value| @options[key.to_sym] = value } if Hash === args.last
args.each { |key| @options[key.to_sym] = true }
self
end