Control flow features and readability2009-05-19 20:46, written by Robert Klemme
First of all I would like to thank our readers who participate in discussions so actively! These discussions provide interesting food for thought as well as inspirations for new blog entries. Today’s article was partly inspired by the question that surfaced recently: “Why is
catch .. throw seen so infrequently?”
In this article I will explore how the choice of control flow constructs and their usage affects readability and maintainability of code. For the purpose of this investigation I will provide a definition, partly because the Wikipedia article seems a bit inconsistent and partly because it may change.
A control flow construct is a language feature which disrupts the normal progression to the next statement and conditionally or unconditionally branches to another location in source code.
This definition includes the usual
if ... then but also method invocation and
Classes of control flow statements
Following a sequence of regular (i.e. non control flow) statements is easy. Things get only slightly more complicated for the reader in case of
if ... then ... else ... end as long as proper indentation is used and we do not have to scroll multiple pages to see the other branch(es).
But it is a different story altogether if the stack frame changes and we as readers have to jump to a completely different location — maybe even in a different file! Hence I will classify control flow constructs depending on the distance measured in stack frames:
- There is no change in stack frame at all.
- Code moves one stack frame up or down.
- An arbitrary amount of stack frames can be added or removed.
- The stack is completely exchanged.
- This is a hybrid where “technical” and “visual” jumps differ.
I am sure those first three categories do not bear any surprises for you. The last two probably need a bit of explanation. Category level-x contains Ruby’s continuations. I won’t cover them here because I am not too familiar with them, they are rarely used in “ordinary” code and they are so complex that they probably deserve an article of their own. It should be obvious that this complexity does not really help understanding code.
This category includes
if ... then ... elsif ... else ... endas well as
- ternary operator
... ? ... : ...,
- both forms of
- all statement modifiers (
ifetc. at the end of a statement).
All these provide for easy following of code and can — when chosen wisely — even form Ruby code which almost reads like English. There are really only two things that can make them hard to follow
- Not indenting code properly.
- Putting too much lines of code between different keywords belonging to the same construct.
As long as you follow these basic rules, the reader of your code will be able to follow the flow easily.
In this category we have
- method invocation,
- method return.
Note, that I did not place
return in this category — we will see it in level-1n below.
The readability of a method invocation depends mostly on how good it conveys what the method does. A Ruby programmer who reads
x.to_s immediately knows that this expression returns a string representation of
x. (Strictly speaking there is of course no guarantee that this method actually returns a String instance but for all practical purposes we can safely assume this.)
For unknown methods the name is crucial for our understanding — or at least rough idea — of what happens during the method call. This is important because only if we have this understanding we can continue reading and understand what the current method does. This shows how maintainability not only depends on proper modularization but also on well chosen method names. In fact it might be more important to get a method’s name right than to have documentation which covers all aspects of the method’s semantics. Don’t get me wrong, I do not want you to neglect your documentation! But a proper chosen name goes a long way in telling the reader of the code that uses this method what it does.
Again, there are two contenders:
raise ... rescue,
catch ... throw.
Although these two may seem to only differ in syntax on first sight, there are fundamental differences. I believe that these are ultimately the reason why we see exceptions quite frequently while we rarely discover a
catch statement in code. Let’s first look at exceptions:
The power of exceptions comes from decoupling the signalling of an error and its handling. The meaning of the error condition is engraved in the exception class (via inheritance and documentation). We know that
Errno::ENOENT denotes a non existing file and we can write an exception handler for this. When we
raise this exception we do not know how many stack frames upwards there will be a handler for it — and we do not need to. If there is none ultimately the interpreter will exit with an error message and an exit code != 0.
Contrast this with
catch ... throw — everything seems to be the opposite here:
- This combination is for regular control flow and not for dealing with error situations.
- You’ll first see the
- There is strong coupling between the
catchlocation and the
throwlocation in code via the symbol used; and both statements can be in different methods altogether.
Now, if you indeed place
throw in different methods you have established a strong link between the two: in order for the program to work properly both need to use the same symbol and both must be aware of the returned object (if any) so the result of
catch can be processed in any meaningful way. You also need to take care about the last statement in the block attached to the
catch in order to not accidentally return something which will be interpreted as thrown return value.
It’s fairly easy to complicate things even more by placing
throw methods in different classes or by nesting two or more
catch constructs. It’s fairly safe to say that these obfuscating effects are best avoided by using
catch ... throw in a single method only — and in that case there are other control flow constructs that we can usually use. In fact I am still searching for more convincing examples of
catch ... throw usage; so far the best contender has been a jump out of multiple nested loops. Although, to me the “multiple nested loops” item has a slight code smell of its own. But, read on…
Actually, you can find a good application of
catch ... throw in Ruby’s standard library. In this case it is elegant and
catch ... throw has the advantage of not interfering with exception handling: assume more complex code in the block which is passed to
Find.find which has a
rescue internally then using exceptions behind the scene might have surprising effects.
You might be curious why I came up with this category. Let’s first look at an example: assume there is a method that yields all Ruby files which are found below any number of directories and we use that to write another method which returns the first of those files that we own:
def find_my_ruby_file(*dirs) find_ruby_files *dirs do |f| return f if File.stat(f).owned? end nil end
return is executed
find_my_ruby_file exits and returns
f, we go up one stack frame. Visually this is true, but in reality, when
return is executed, the stack is several levels deeper than it appears to be. You can easily check that by inserting something like
puts caller(0) before the
return – here’s a complete version that you can use to experiment:
require 'find' def find_ruby_files(*dirs) Find.find *dirs do |file| yield file if test ?f, file and /\.rb\z/i =~ file end ensure puts 'exit find_ruby_files' end def find_my_ruby_file(*dirs) find_ruby_files *dirs do |f| if File.stat(f).owned? puts 'found!', 'stack <<<', caller(0), '>>> stack' return f end end nil ensure puts 'exit find_my_ruby_file' end p find_my_ruby_file(*ARGV)
You’ll find some surprising entries there and as a side effect I now know one more example where
catch ... throw does seem like the best tool for the job…
This property of being able to exit more stack frames than visible on first sight is shared by
break which can be used to exit multiple stack levels in a similar way. It won’t leave the current method (i.e. the method in which it is lexically scoped) but via invoked methods with blocks the unnesting can include any number of stack frames. This reinforces what we have said before since we now clearly see that there are more ways that a block of code can be left “not normally” besides
What do we learn?
Looking at control flow constructs from an execution stack perspective has provided some interesting insights (at least to me). While general rules for readability (proper indentation, limit the amount someone has to read etc.) do apply here as well, some of those constructs can have surprising effects on program behavior and should be used carefully. This is especially true for the “far reaching” constructs which can actually make the stack unwind multiple levels — or even change completely as in the case of