The functional programming core concept is about composing functions together to craft more complex and convoluted functions. In Scala (not unlike what happens in many other programming languages) there are two ways to combine functions: the first is to apply a function to the result value of another function and the latter is to use a function to produce the argument value of another function.
Written in this way they may look not so different, in fact, even if in practice they show up in quite a different look, the abuse of the mechanism leads to the same problem.
The first way of combining functions, is also called chaining since you chain functions together – the result of function fn is the input of function fn+1. (Interestigly this very same mechanism will get a boost in C++ starting from C++20 ISO standard thanks to range and pipes).
Take the following example:
List(1,2,3,4).filter( _ > 2 ).map( _.toString ).fold( "" )( _ + _ )
(If you already know Scala, you may safely skip this paragraph 🙂 ) If you are unfamiliar with Scala or function programming this may look suspiciously like bad code, (this could be some meat for another post), but trust me it is not. Function List(…) constructs a List. List, containers, and iterators in general in Scala have a set of methods that can be chained together. You may think of them as taking an iterator and producing an iterator in a different sequence. Back to the example above,
filter produces an iterator that scans only elements of the input sequence that fulfill the given condition (being greater than 2 in the example).
map produces an iterator over a sequence whose elements are computed by the given function on the source elements. Eventually
fold collapses the sequence by accumulating items iteratively using the provided expression.
Each small block performs an operation. Note that I haven’t written “simple” operation, on purpose. In fact the operator (
fold) is simple in itself, but it is programmable via a lambda that can be as convoluted as you want. Therfore the operation performed by the block may become really complex.
While you are focused on your goal, it may be easy and convenient to dump your mind into an endless chain. This has two drawbacks, first, it is unreadable and second, it is uninspectable.
It is unreadable because you need to start from the beginning and analyze the sequence up to the point to where you want to focus and know what the inputs are and from there onward to understand how the output is processed into the final result. Pair this with complex lambdas and you may find yourself in quite a nightmare to figure out what this code is supposed to do or where is the bug.
It is uninspectable because you cannot use the debugger to stop execution and show intermediate results of your operation (I’ve noticed over the years that Scala programmers are usually reluctant in using a debugger preferring print/log debug).
The other form of the problem – functions calling functions calling functions – despite the differences in syntax, is not that different in how it is produced and in the result. The code you may see, such as –
context.become( enqueuer( resourceManagement( Queues( queues.execution.filterNot( x => ( x == realFree || context.child( childName( x.getUUID ) ).isEmpty ) ), queues.waiting ) ) ), true )
can be written in any language, but it seems that our fathers was quite fond of this approach.
It is important to recognize that when writing code you have (or, at least you should have) a crystal clear and detailed comprehension of the data flow, and even if you strive to write clear code, you’ll tend to pack these things together because a) it is faster (less typing, less naming of values, less effort to add structure) and b) you don’t feel the need to make it clearer since it is so straightforward in your head.
Lesson learned – resist the temptation of dumping the mental flow into a coded data flow, use the old “divide et impera” to split the flow into manageable units with names reflecting the content e the intent. A good indication is the line length, if your statement, properly formatted to fit in 80 columns, happens to span over more than 3 lines, then splitting it is a good idea.
Additional thought Attending some FP conferences I had the clear impression that FP pundits encourage complex aggregation of functions, even the justification for having short indent space (2 characters is the recommended indentation for Scala) has the purpouse of make the writing of chains more convenient. For these reasons I suspect that this point could be a little controversial in FP circles. I stay with my idea that industrial quality code must be first easy to read and understand and then easy to write.