This post originally appeared in my Chainline Newsletter. Due to popular request, I'm re-publishing it here on my blog. It has been lightly edited.
As part of my local ruby meetup (#westendruby), I've been dabbling in katas and quizzes. Having worked several, I can't help but notice that my solutions are sometimes radically different from the others.
Hmmm.
Having one's solutions differ from the crowd's is (and ought to be) a cause for heightened scrutiny. Therefore, I've been pondering code, trying to understand what's driving my choices. I have a glimmer of an idea, and thus, this newsletter.
The Setup
The easiest way for me to explain is for you to first go do the Roman numerals kata. Happily, there's a Roman numerals test on exercism to get you started. The task is to convert Arabic numbers into Roman numerals, and the tests are all some form of:
assert_equal 'I', 1.to_roman
or
assert_equal 'CMXI', 911.to_roman
In case your life is such that you can't drop everything and do that exercise right now, here's a reminder of how the Roman counting system works. There are two main ideas. First, a few specific numbers are represented by letters of the Roman alphabet. Next, these letters can be combined to represent other numbers.
Seven Roman letters are used. The letters and their associated values are:
- I = 1
- V = 5
- X = 10
- L = 50
- C = 100
- D = 500
- M = 1,000
1-10 in the Arabic numbering system is I, II, III, IV, V, VI, VII, VIII, IX, and X in Roman numerals. As you may already know, there are two rules at work.
1, 2 and 3 illustrate the first rule. 1 is one I. 2 is two Is. 3 is three Is. The rule is: for Arabic value x, select the largest Roman letter that's less than x, and generate x number of copies. Let's call this the 'additive' rule.
4 follows the second rule. Instead of IIII (four I's), 4 is written as IV (one less than five). This rule is: select the first Roman letter higher than the Arabic value, and prefix it with the adjacent lower letter. This rule is used in cases where 4 sequential occurrences of any letter would otherwise be necessary. Thus, 4 is IV instead of IIII, 9 is IX instead of VIIII, etc. Let's call this the 'subtractive' rule.
Now, consider the code needed to satisfy this kata. Given that there are two rules, it seems as if there must be two cases. To handle the two cases, it feels like the code will need a conditional that has two branches, one for each rule.
The actual implementation code might be more procedural (the conversion logic could be hard-coded into the branches of the conditional) or more object-oriented (you could create an object to handle each rule and have a conditional somewhere to select the correct one), but regardless of whether you write a procedure or use OO composition, there's still a conditional.
The Insight
I hated this. Not only did I not want the conditional, but figuring out when to use which rule seemed like a royal PITA. It felt like the conditional would need to do something clever to select the correct rule, and I wasn't feeling particularly quick-witted. Thus, I found myself pondering this kata with a faint sense of dread, while the meetup loomed.
Regardless, I sat down to write some code, and immediately realized that although I was faintly aware that there were two conversion rules, I didn't know the full set of Roman letters and their associated Arabic values. I then consulted the wikipedia page for Roman numerals, where I found something which gave me a dramatically simpler view the problem. Serious lightbulb moment.
It turns out that the way we think about Roman numerals today is the result of an evolutionary process. In the beginning, they were uniformly additive. 4 was written as IIII and 9, VIIII. As time passed, the subtractive form crept in. 4 became IV, and 9, IX. In modern times we consistently use the shorter, subtractive form, but in Roman times it was common to see one form, or the other, or a combination thereof.
This means that the additive form is a completely legitimate kind of Roman numeral. (Who knew?) It can be produced in its entirety by rule 1, which is comfortingly simple to implement. The conversion from additive to subtractive is also dead easy, and can be accomplished via a simple mapping that encodes rule 2.
The key insight here is that converting from Arabic to additive Roman is one idea, and converting from additive to subtractive Roman is quite another. Solving this kata by converting Arabic numbers directly into subtractive Roman skips a step, and conflates these two ideas. It is this conflation that dooms us to the conditional.
Having had this realization, I wrote two simple bits of code. One converted Arabic to additive Roman, the other additive to subtractive Roman. Used in combination, they pass the tests.
I took the code to #westendruby, where someone pointed out that not only was my variant more understandable than many other implementations, but also that it could easily be extended to perform the reverse conversion. They were absolutely right; it took just a few lines of additional code to convert from Roman numerals back into Arabic numbers. Adding this new feature to other implementations was far more difficult.
I wrote several versions of the kata. Here's the one I ended up liking the best.
The Upshot
I left that meetup with a newfound respect for what it means to have a conditional.
Conditionals are trying to tell you something. Sometimes it is that you ought to be using composition, i.e., that you should create multiple objects that play a common role, and then select and inject one of these objects for use in place of the conditional. Composition is the right solution when a single abstract concept has several concrete implementations.
However, rule 1 and rule 2 above don't represent alternative implementations of the same concept, instead they represent two entirely unrelated ideas. The solution here is therefore not composition, but instead to create two transformations, and apply them in order. This lets you replace one "special" case with two normal ones, and reap the following benefits:
- The resulting code is more straightforward.
- The tests are more understandable.
- The code can produce the pure additive form of Roman numerals, in addition to the subtractive one.
- The code is easily extended to do the reverse conversion.
The keystone in this arch of understanding is being comfortable with transformations that appear to do nothing. It is entirely possible for a Roman numeral to look identical in its additive and subtractive forms. III for example, looks the same either way. Regardless, the additive III must be permitted to pass unhindered through the transformation to subtractive. You can't check to see if it needs to be converted, instead you must blithely convert it. This makes everything the same, and it is sameness that gets rid of the conditional.
The Commentary
Now, if you'll permit, I'll speculate. I'm interested in why this solution occurred to me, but not others. Folks at the meetup found it startling in its simplicity and utility. Once known, it seems inevitable, but before knowing, inconceivable.
What would someone have to know in order to be able to dream up this solution? How can we teach OO so that folks learn to look at similar problems and recognize the underlying concepts? What quality in my background or style of thinking revealed them to me? Mind you, I'm not saying that my solution is perfect, but it's certainly different. Why?
I think there are two reasons. First, I'm committed to simplicity. I believe in it, and insist upon it. I am unwilling to settle for complexity. Simplicity is often harder then complexity, but it's worth the struggle, and everything in my experience tells me that with enough effort, it's achievable. I have faith.
Next, the desire for simplicity means that I abhor special cases. I am willing to trade CPU cycles to achieve sameness. I'll happily perform unnecessary operations on objects that are already perfectly okay if that lets me treat them interchangeably. Code is read many more times that it is written, and computers are fast. This trade is a bargain that I'll take every time.
So:
Insist on simplicity.
Resist special cases.
Listen to conditionals.
Identify underlying concepts.
And search for the abstractions that let you treat everything the same.
Thanks for reading,
Sandi
News: 99 Bottles of OOP in JS, PHP, and Ruby!
The 2nd Edition of 99 Bottles of OOP has been released!
The 2nd Edition contains 3 new chapters and is about 50% longer than the 1st. Also, because 99 Bottles of OOP is about object-oriented design in general rather than any specific language, this time around we created separate books that are technically identical, but use different programming languages for the examples.
99 Bottles of OOP is currently available in Ruby, JavaScript, and PHP versions, and beer and milk beverages. It's delivered in epub, kepub, mobi and pdf formats. This results in six different books and (3x2x4) 24 possible downloads; all unique, yet still the same. One purchase gives you rights to download any or all.