"L" is not a code smell
During
my presentation at RubyFringe, I shared a question that has been swirling around in my brain for a while:
Are IDE features really language smells?
I don’t think it’s an original thought. If nothing else, it’s a corollary to what I believe to be true about many of the GoF design patterns: Many of them are workarounds, ways to Greenspun missing language features. Now, this is probably not the case for all IDE features, and in truth it may be that there are some features which could be implemented in either the language or the IDE, but the IDE may be the best place to put them.
But there is a fairly large class of IDE features that strike me as language workarounds. One of them is definitely the ability to spit out a lot of boilerplate. If you need a lot of code written, you ought to be able to get your programming language to do it for you, not your IDE.
There is room for people to disagree about this. There are some who feel (Strawman alert!) that programs consisting of large numbers of simple elements are easier to understand than programs consisting of a small number of highly abstract elements. Those folks feel an IDE gives you the ease of writing a program quickly plus the ease of reading that same program quickly. They feel that abstractions make the program easier to write but harder to read.
I happen to disagree with this, and if you have been reading this weblog for more than a couple of days you have already read why my experience leads me down a different path. Although in deference to my colleagues with different views, I offer this quote:
All problems can be solved by adding another layer of abstraction, except the problem of having too many layers of abstraction.
Anyone who has dealt with an
hammer factory will agree.
So back to “L.”
Two speakers before me,
Giles Bowkett gave his excellent
Archaeopteryx—um—presentation. I hesitated over that word, because I could just as easily say
performance. Performances are terrific entertainment, but they sometimes obscure the message behind them. I want to say outright that while this is true of many other subjects, I felt it worked for Giles because the subject of his presentation was software development as a
how rather than a what, and for Giles the “what” is performance.
(Giles got a standing “O,” and many people might be tempted to rush out and make their presentations just as stimulating (400+ in-your-face slides punctuated with loud, driving
drum and bass). Be sure that your material matches your presentation style! If not, people may walk away saying “Wow, amazing, but what exactly did she say?” I think it worked for Giles and that’s quite an accomplishment.)
Now really, back to “L.”
Giles is one of the people using closures in Ruby. Meaning, he is passing functions around and storing functions in objects. I am not going to try to say exactly what Archaeopteryx does, so I will describe this style of programming using an imaginary companion program that creates walking bass lines. I will call it
Troody.
Let’s simplify things greatly and say that Troody will only ever play in perfect 4-4 time and further that Troody only ever play one of the eight notes in a particular chord’s standard scale. The probability of playing each of those notes on any one “beta” could be represented as an array with eight elements, like this: [.35, .05, .1, .05, .25, .05, .1, .05 ]. You can imagine passing arrays like this around in Troody.
For example, we can pass this array to an object that actually plucks the strings: Plucker.new.start_plucking([ 0.35, 0.05, 0.1, 0.05, 0.25, 0.05, 0.1, 0.05 ]).
Let’s try writing a naïve Troody Plucker:
class Plucker
def start_plucking(probs)
while (self.tune.playing)
if (Metronone.on_the_beat)
r = rand
cumulative_probs = probs.inject([]) { |cum, element|
cum + [ cum[-1] && (cum[-1] + element) || element ]
}
notes_to_cumulative_probs = (1..8).zip(cumulative_probs)
note_to_play = notes_to_cumulative_probs.detect { |note, prob| prob >= r }
self.pluck(note_to_play)
end
end
end
end
You pass it a set of probabilities, it produces bass notes. But stop, that’s so procedural. Let’s learn from a flying creature, let’s learn from Archaeopteryx. Instead of passing arrays, let’s pass
lambdas, like this: lambda { [.35, .05, .1, .05, .25, .05, .1, .05 ] }. Now whenever Troody needs the probability of something, we call the function with .call or Ruby’s [] alternative syntax. So now we write Plucker.new.start_plucking(lambda { [.35, .05, .1, .05, .25, .05, .1, .05 ] })
Our new Plucker code is the same as the old, except we write:
cumulative_probs = probs.call.inject([]) { |cum, element|
cum + [ cum[-1] && (cum[-1] + element) || element ]
}
We now call the probs lambda when we need a note. That’s it, we’ve added a .call call. What does that get us? Well, here’s one thing: If we want the probability to change over time, our function can do that, and we don’t have to rewrite our start_plucking method to handle the idea.
For example, here’s a probability lambda that usually plays the same way but from time to time decides it ought to play pedal notes (refactoring to OO is an optional exercise):
probs = lambda { |bars_of_pedal, beat|
lambda {
if beat == 0
if bars_of_pedal == 0
bars_of_pedal = 1 if rand < .05
elsif bars_of_pedal == 5
if rand < .25
bars_of_pedal = 0
else
bars_of_pedal += 1
end
elsif bars_of_pedal == 9
if rand < .5
bars_of_pedal = 0
else
bars_of_pedal += 1
end
elsif bars_of_pedal == 13
bars_of_pedal = 0
end
end
beat = (beat + 1) % 4
if bars_of_pedal == 0
[ 0.35, 0.05, 0.1, 0.05, 0.25, 0.05, 0.1, 0.05 ]
else
[ 1.0, 0, 0, 0, 0, 0, 0, 0 ]
end
}
}.call(0, 0)
Thanks to the way we’ve separated the probabilities from the plucking, we do not need to subclass Plucker to try a different playing style in Troody.
As Giles pointed out, this is the
Strategy Pattern. We are making different kinds of pluckers by encapsulating the logic of what to pluck in something we pass to a plucker. Archaeopteryx appears to do this everywhere. There are lambdas paramaterized by lambdas, lambdas that return lambdas…
This creates a problem. Imagine a programing language where all the keywords are in upper case: IF foo THEN bar ELSE bizzat. Try reading such a program aloud, and you end up shouting the punctuation but speaking the words. This is wrong! We should be shouting the words and whispering the punctuation!
And the problem with Ruby’s lambdas is that if you use a lot of them the word lambda really starts to stand out. So Giles fixed this by aliasing it to L: and using [] instead of .call():
alias :L :lambda
L{ |a| a + a}[5].
=> 10
Much nicer, and as Giles pointed out, this is an example of Ruby’s strength. If you have a program that rarely uses lambdas, you probably want lambdas to stand out when you use them, so you don’t alias lambdas to L and you use the call method, not the square brackets. But if you use lambdas a lot, it’s a win to abbreviate things.
Okay, we’re talking about “L.” Good.
Now in my talk, I said that abbreviating lambda to L was a code smell. I was wrong! Giles, my bad!!
What I actually think is that needing to abbreviate lambda to L is a language smell. Very different. If you show me a Java program and you show me Strategy Pattern, I shouldn’t say it’s a code smell. I should say too bad for you that you need all that boilerplate when Ruby lets you do that with the word lambda and a pair of curly braces.
So now to “L:” Giles, if lambdas are integral to Archaeopteryx, if they are so woven into the fabric of what Archaeopteryx does that you want the keyword “lambda” to fade away, I honestly think this is a place where the language
could help you.
For example, what if Ruby had
call-by-name semantics? You could write:
Plucker.new.start_plucking([ 0.35, 0.05, 0.1, 0.05, 0.25, 0.05, 0.1, 0.05 ])
# or...
bars_of_pedal, beat = 0, 0
Plucker.new.start_plucking(
if beat == 0
if bars_of_pedal == 0
bars_of_pedal = 1 if rand < .05
elsif bars_of_pedal == 5
if rand < .25
bars_of_pedal = 0
else
bars_of_pedal += 1
end
elsif bars_of_pedal == 9
if rand < .5
bars_of_pedal = 0
else
bars_of_pedal += 1
end
elsif bars_of_pedal == 13
bars_of_pedal = 0
end
end
beat = (beat + 1) % 4
if bars_of_pedal == 0
[ 0.35, 0.05, 0.1, 0.05, 0.25, 0.05, 0.1, 0.05 ]
else
[ 1.0, 0, 0, 0, 0, 0, 0, 0 ]
end
)
And you would get the same behaviour as if you were using lambdas.
That’s it, that’s what I should have said on stage: any time you are working around your language—whether in your IDE, or by modifying open classes, or by abbreviating things—that’s a place where we should step back, where we should ask if our language is missing something.
The answer may very well be “no.” But we ought to at least ask the question.