Functional imperative, functional declarative
This post is a part of the F# Advent Calendar 2018. Be sure to check out other submissions!
For quite a long time I used to believe that your code can either be functional or imperative — it can’t be both at the same time. I think it mostly boiled down to my C#-centric point of view, where doing for loops is usually considered an imperative approach when declarative LINQ queries could be used instead. Compare:
var resultList = list.Where(e => e > 2).ToList();
When I started programming in F#, I noticed that a lot of code written in that language leans towards being declarative in nature, probably thanks to the features of the language like immutability by default or discriminated unions. But recently I stumbled upon a piece of code which looked somewhat like that:
So, what does this code do? It publishes messages to the queue (as the function name says). But it also does some manipulations with the passed messages list.
Upon closer inspection I realized that it is a recursive function. On each call, it splits the passed messages list into two parts — a current batch and a reminder, handles the batch at hand, and then calls itself passing reminding messages as an argument.
Is this code functional? Well, it is literally a (recursive!) function, and it does not mutate any state. It causes side effects, but in my opinion in the realm of F# this doesn’t disqualify from being a functional approach.
But, crucially (for the purpose of this post ;)), is it declarative? I wouldn’t say so. The level of details involved in controlling the algorithm — like the stopping case for a recursive call — hides what the major goal is here, which is sending messages in batches. This is similar to the first listing of this post, where details of constructing returned list obscures what is really interesting — the transformation applied to the input elements.
I think the code mixes two separate concerns — handling the list and handling the messages, which makes it hard to figure out what’s going on at a glance. The better approach would be then to separate the two concerns — do the batching first, then do the sending.
First, let’s steal some code from fssnip:
Now, we can refactor our code:
I think the resulting code is more declarative and easier to understand, without being any less functional.
This may be a pretty silly example of refactoring, but I think it reveals a few interesting points.
Firstly, using recursion is invaluable in some places (like parsing trees) but if it can be avoided, the resulting code is in my experience easier to follow. This is simply because a person reading the code does not need to keep track of all the details of the algorithm like stopping condition. Not to mention the “huh” factor — “ok, this function publishes a list of messages — wait what, why is it recursive?”
Secondly, in online discussions I saw people objecting to even having for loops in their F# code, which was deemed “not functional” approach to solving problems. The terms like “functional”, “declarative” and “imperative” are often used in important discussions like code reviews or job interviews, so I think it is really important to think a little bit what definitions are we using for those terms and how well do they work when applied to some real, messy, production codebases.
Lastly, when writing this post, I googled “imperative functional programming” and lo and behold, a whitepaper written Simon L Peyton and Jones Philip Wadler showed up. So if you want an actual insight, go read it, and in the meantime please take all this rambling above with a proper grain of salt ;)
Merry Christmas and happy F#ing!