Control structures have been around nearly as long as programming but it's hard for me to see them as more than an annoyance. Over and over again, I find that better code has fewer if-statements, fewer switches, and fewer loops. Often this happens because developers are using languages with better abstractions. They aren't consciously trying to avoid control structures but they do.
If we are working in an object-oriented language, we can replace switch-statements with polymorphism. The same trick works well for if-statements too, but it can be overkill in simple cases. When we use languages with functional features, we can do most of the work that we do in loops using maps, filters, and folds. Control structures end up disappearing, and that can be a good thing.
The problem with control structures is that they often make it easy to modify code in bad ways. Let's take a simple if-statement:
if ...
...
else
...
end
Every place that we have ellipses in that code is a place where we can put more code. Those places can access variables outside of the if. It's very easy to introduce coupling. Moreover, people do routinely nest conditionals inside of conditionals. Some of the worst code I've ever seen is a cavernous nightmare of nested conditions with odd bits of work interspersed within them. I suppose that the real problem with control structures is that they are often mixed with the work. I'm sure there's some way that we can see this as a form of single responsibility violation.
What can we do? Do we have to live with control structures? In general, I think that we do, but it's an interesting exercise to see what we can do to reduce our use of them. Often we can learn new tricks and make our code clearer in the process.
A while ago, I was working on some Ruby code and I needed to write a 'take' function to take elements from the beginning of an array. Ruby already has a take function on Enumerable, but I needed to special behavior. If the number of elements I needed was larger than the number of elements in the array, I needed to pad the remaining space in the resulting array with zeros.
This seems like a job for a simple if-statement:
def padded_take ary, n
if n <= ary.length
ary.take(n)
else
ary + [0] * (n - ary.length)
end
end
Let's look at this code carefully. There's nothing in it which tells us anything about what padding is, and how the pad relates to the array we're making. We can see what is happening if we look close, but we don't see the concepts in the code.
We could introduce some functions to make it clearer, and simplify the conditional by using a guard clause:
def padded_take ary, n
return ary.take(n) unless needs_padding?(ary, n)
ary + pad(ary, n)
end
That's short and sweet but it doesn't take advantage of a simple fact - we can use a null object to get rid of a conditional. An empty array is a wonderful null object. Let's start over.
We don't need a conditional to compute the length of the pad. The length is number of elements we want to take minus the length of the array, if the number we want to take is greater than the array length - it's the maximum of the difference and zero:
pad_length = [0, n - ary.length].max
If we have that, we can pad the array first and then take the number of elements we need from it:
def pad ary, n
pad_length = [0, n - ary.length].max
ary + [0] * pad_length
end
At this point, we can write our padded take:
def padded_take ary, n
pad(ary, n).take(n)
end
What we've done is eliminate an if-statement by forming a computation that always appends a pad. However, sometimes that pad is just an empty array.
I'm not going to argue that this code is simpler than the if-then-else code we started with but it is more declarative and, in general, I don't think that code using this strategy is as prone to abuse.
There's a clear advantage to being able to think at this level of abstraction. It pays dividends when we confront larger problems.