Composition is at the heart of writing software well.
One reason is that software must be written for people to read and understand. When this is not a priority a system will take on additional technical debt as the software begins to deteriorate into legacy code. Bear in mind, that code would not be called that if it remained well understood and easy to change and extend.
Composition is first about well-defined responsibilities. A component should know how to perform a particular task and do it well; it shouldn't even be aware that there are other kinds of tasks. Composing by it's very nature is about isolating concerns. When components are designed this way it makes them easy to glue together to perform more complex tasks. This property is superior to most other properties when it comes to writing systems that stand up to the pressure of ongoing change.
Take a list of employees. Each year we run a process that takes all employees having worked 5 years or more and gives them an extra paid day off.
DB. GetEmployees(). Where(emp => emp.getYearsEmployed() >= 5). Select(emp => emp.getBenefits()). Each(bene => bene.addPTODay(1))
For Each emp in DB.GetEmployees() var bene = emp.getBenefits() If bene.getYearsEmployed() >= 5 Then bene.addPTODay(1) End Next
Note the differences.
Each step in the functional approach isolates a concern whereas the imperative approach intermingles them into a more complex unit of work. The functional program at one moment is concerned with filtering (
Where) and a moment later projecting (
Select). It deals with these concerns one at a time composing them into a greater concern. The imperative approach balls them up.
Consider the practicality. Using the functional approach one has an easier means of executing just part of the composition. In this situation, it might be useful, in a REPL for example, to to select the targeted employees and confirm we have the right ones, before executing the side effects. You might liken this to running the a
SELECT before you actually run your
UPDATE in T-SQL. This isn't quite as easy with imperative code.
The functional way is more modular and human parsable, if you will. It's easier to insert other components into the pipeline because the concerns are not intermingled. It's equally easy to drop a concern from the pipeline. Let's give everyone an extra day off: just drop the
Where. Doing the same to the imperative code would involve dropping the
If, removing the
If block terminator, and unindenting the code. This lifts code from being conditional to unconditional but the manner is less straighforward and thus more likely to be done incorrectly. If many more concerns had been involved this would have been even more perilous.
The functional approach pushes the side effects (state changes) to the end more neatly separating them from the semantics of finding the right data on which to act. The imperative approach performs the actions somewhere in the middle of the block. The work is performed in a larger, more-deeply-nested context which has references to variables that are not necessarily relevant to the work happening on any given line.
I keep stressing how trivial this example is. A process can be way more involved potentially having dozens of variables and dealing with several conditional branches. One could imagine nesting 3, 5, even 7 levels deep. The functional model holds up better under these circumstances because it works hard to isolate concerns and treats them as components. When it's all said and done, composition is at the very heart of functional programming. Programs are built up compositionally from bottom to top.
So what does imperative programming do better?
Well, the fact that a process is intermingled rather than composed will allow it to be more highly optimized, meaning faster — though the functional style does in some instances outperform the imperative style.
I have generally found that programmers find it easier to write programs imperatively. I chalk this up to institutional training (we've been taught this way of thinking from early on) and the fact that I think it does better match how normal people think and reason about solving real-world problems.
Because the imperative way freely permits mutation, it's generally easier to effect state changes (save data). In the above functional example, I performed a mutation when I added the paid day off. If I had been more strictly functional I would have used an even more isolated method of effecting the change. This is because the functional paradigm wants to deal primarily in immutable data.
Having immutable data by default forces a programmer to effect state changes in a more constrained way. This is a double-edged sword. In one respect it forces programs to be written in a style that make them easier to reason about. In another, it adds a layer of complexity in that you have to devise a strategy for handling state change. This often means that, as demonstrated in the example above, one generally defers dealing with state change until the last possible moment. Only practice will enable one to understand how to do this well. While imperative programmers don't have to deal with this complexity because they can modify state willy-nilly, there's benefit in learning how to isolate state changes from all the other work a program needs to do.
When I think about designing a functional program I think of a tube of toothpaste where the toothpaste is dealing with state change. As best one can he squeezes the toothpaste from the back of the tube up toward the front and out the nozzle. Inevitably, it gets harder and harder to get out that last bit, but you try. And finally, there is a small bit which remains which just won't come. Writing a functional program is like that. While you do everything possible to squeeze all of it out, a bit of it must remain.