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.
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 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
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.
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.
Here's my final version of the code.
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.
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.
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,
Public POOD course coming to New York City!
I'm pleased to announce that I'll be teaching a public Practical Object-Oriented Design course in New York City on Oct 31-Nov 2, 2016, fondly named POODNYC. And (drum roll) I'm doubly pleased to announce that Avdi Grimm, Head Chef at Ruby Tapas, will be the co-instructor.
I created two special tickets just for readers of Chainline, at 25% off. These will be available until they're gone, or through June 7th, whichever arrives first.
If you miss out on the discount, don't be too sad; you haven't missed your chance. Regular tickets are also on sale now!
My new 99 Bottles book is out in private beta, and is slated for general release Any Day Now ™. News and discounts will be announced on the 99Bottles mailing list. This list has almost zero traffic, so you won't hate yourself if you sign up today.