# I Am Not a Functional Programmer
Despite rumors to the contrary, I am not actually a functional programmer. True, I sometimes slip and fall and an "applicative functor" would come out from under my breath. But surely you shouldn't judge me for this minor tick. So no, I'm not a functional programmer, I'm just trying to be reasonable1.
We all want higher code quality, better testability, and less maintenance burden. Don't we? It just so happens that techniques from FP are really good at achieving these goals. We don't even have to mention functional programming to anyone, we can just write better code and call it a day2.
Spooky Action at a Distance
Did it ever happen to you that you needed to implement some minor fix in one place in your codebase, and then suddenly code in another seemingly unrelated place started breaking? If you're lucky the breakage is something loud and clear like an exception. If you're less fortunate it will be some kind of silent and subtle logic bug that takes time and effort to pinpoint.
When this happens we can say that our code suffers from "spooky action at a distance"3. I think that these days it's universally accepted that spooky action at a distance is bad for everyone involved. Who wants to live in a world where small changes have unexpected consequences all over the place?
If we accept the premise that minimizing spooky action at a distance is a net positive for the maintainability of our code, the next question becomes, what are the sources of this malaise?
One prominent source of spooky action is shared mutable data (both variables and data structures), especially high-visibility or even global data. When you mutate data what do you know about all the other potential users of that data? Are you willing to check them one by one to make sure you didn't break any invariants? Can you even tell who they are? Can you?
If you're like most programmers you probably can't, or won't. You make the changes, cross your fingers, and pray for the best. But if we recognize all the pain and suffering that shared mutation brings into our lives (and I didn't even mention concurrency) a reasonable conclusion would be to avoid it like the plague. Mark everything as const/final, favor immutable data structures, and breathe a sigh of relief.
You'll be surprised how good it feels. No need to defensively copy anything, no need to fear the consequences of mutating this or that input (you can't). Debugging becomes easier. Sure, you'll have to learn some new ways of solving problems, but spooky action at a distance is scientifically guaranteed to drop by at least 50%.
Mocking the World
Most programs do more than mutating variables. They do a whole lot of things, like talking to databases, or sending HTTP requests, and many other interactions with the big bad world. And that's perfectly normal. We're all conscientious developers though, and before we ship anything we must verify that it actually works as stated. Some of us even write tests to try and prove that, or so I hear.
But my god, writing tests has become so tedious...
Even for the smallest test that validates some simple business rule you have to set up 15 different mocks/spies/doubles/clones/whatever. One for the DbFactoryProxyFactory, another for the HttpCoordinationOrchestratorManipulator, and the list goes on and on. Usually these mocks tend to be annoyingly mutable and prone to race conditions.
Sure, these days you can delegate some/most of this tedium to your handy AI, it won't complain (yet4). Unfortunately, despite a common misconception, tests are not "write only", they are also a form of executable system documentation. Even if you didn't have to write all that noise yourself, you might need to read it one day and the signal to noise ratio makes it a huge waste of time just trying to locate the code that actually matters.
If we take a closer look at the sort of code that forces us to write these unpleasant tests, we are likely to find that it entangles5 too many things together. Or at least two of them: business logic and interactions with the external world.
Unless you're doing some very complex external interactions it's quite likely that you should focus most of your testing effort on the business logic. Presumably that's also where most of the value for the business lies as well. Too bad it's so difficult.
Since most of the complex mocking described above comes from the bits of code that interact with the external world, this should give us plenty of motivation to disentangle the business logic from the interaction code. Business logic tends to be simple in-memory computation (at least conceptually), testing it in isolation should be much simpler. Set up some inputs, assert some outputs, done. Written this way, the signal to noise ratio likely to go way up.
The actual process of disentangling code might not be easy. It might even require some deep design changes in how you structure your code. But if you're serious about testing it is well worth the effort. And as the people from the cult of TDD will tell you, code that is easy to test tends to be of all around higher quality. You get this for free just by focusing on less tedious tests.
Feeling a Bit DRY
I don't like repeating myself6, both because I'm lazy and because it just itches me the wrong way to see the same code repeated again, and again, and again... I suspect that many programmers share this tendency naturally. And even if not, we can probably agree that more code equals more liability (tests, maintenance, bugs) and so when it doesn't obstruct readability, reducing code volume7 is probably for the best.
Suppose that you're coding yet another shopping cart backend for whatever. You now create the cart checkout process. This would've been straightforward but there are quite a few failure modes8, and your team is obsessed with logging and error enrichment. So you end up with this code:
function processCustomerCheckout(order) { let r1 console.log('Validating inventory...') try { r1 = validateInventory(order) // 1 } catch (e) { console.error('Validating inventory failed:', e.message) throw Error(`Validating inventory: ${e.message}`) }
let r2 console.log('Calculating shipping...') try { r2 = calculateShipping(r1) // 2 } catch (e) { console.error('Calculating shipping failed:', e.message) throw Error(`Calculating shipping: ${e.message}`) }
let r3 console.log('Processing payment...') try { r3 = processPayment(r2) // 3 } catch (e) { console.error('Processing payment failed:', e.message) throw Error(`Processing payment: ${e.message}`) }
return confirmOrder(r3) // 4}There are four steps to the checkout process: validating the inventory (1), calculating shipping costs (2), processing the payment (3), and confirming the order (4). Unfortunately all the error handling and logging that's interspersed between the steps makes it very difficult to actually see what's actually going on.
But it's the repetitiveness that really gets me, I have to DRY it out to oblivion.
If we look at what's fixed vs. changing we see that in each step we process the order and produce the output for the next step, and every step is wrapped with almost identical logging and error handling.
One common way to reduce simple repetition like this is to use an anonymous function to extract the part that changes in the different steps9. So we move the logging and error handling into a helper function like so:
// 1function withLogging(stepFn, stepName, order) { console.log(`${stepName}...`) try { return stepFn(order) // 2 } catch (e) { // 3 console.error(`${stepName} failed:`, e.message) throw Error(`${stepName}: ${e.message}`) // 4 }}This new withLogging helper function (1) takes a function argument for the step (stepFn), the name for the current step, and the original order input. It then applies the logging and error handling logic from before: log and call the stepFn function on the order argument (2). If there is an exception (3), log, "enrich", and rethrow it (4).
We can now use withLogging to wrap around our original steps and see the repetition melt away:
function processCustomerCheckout(order) { const r1 = withLogging( validateInventory, 'Validating inventory', order) const r2 = withLogging( calculateShipping, 'Calculating shipping', r1) const r3 = withLogging( processPayment, 'Processing payment', r2)
return confirmOrder(r3)}Much better, it's now easier to spot the building blocks of the flow. But it's still kind of annoying. With the plumbing still sticking out and obfuscating the main logic. Do we actually care about the logging strings? Or the fact that logging is present here?
These are all good questions, but for me this is still just repetitive. So we'll use a bigger hammer to take it out. Let's hide away the logging completely by wrapping away the original functions:
function withLogging(stepFn, stepName) { // 1 return (order) => { // 2 console.log(`${stepName}...`) try { return stepFn(order) } catch (e) { console.error(`${stepName} failed:`, e.message) throw Error(`${stepName}: ${e.message}`) } }}This withLogging variant is very similar to the first one, except that it's no longer taking the order as a top-level argument (1). Instead it produces a new function (2) that expects the order argument and wraps around the invocation of stepFn10.
We use the new withLogging function by pre-applying it to our building blocks:
const checkInventory = withLogging(validateInventory, 'Validating inventory')const withShipping = withLogging(calculateShipping, 'Calculating shipping')const withPayment = withLogging(processPayment, 'Processing payment')With the wrapped building blocks we can rewrite the flow as follows:
function processCustomerCheckout(order) { const r1 = checkInventory(order) const r2 = withShipping(r1) const r3 = withPayment(r2)
return confirmOrder(r3)}Without all the logging noise, this is almost what we want, but for the last bit of obfuscation.
Do we enjoy passing around the r1, r2, r3 variables? It's not just annoying, but also error prone, what if we add another step and forget to update the numbering appropriately11?
What we really want is to just say "here are the steps, pipe them together", this should eliminate the last bit of repetition:
function pipe(...fns) = initialValue => fns.reduce((value, fn) => fn(value), initialValue)We are going a bit meta, but given a list of functions we create a new function that applies them one after the other starting with some initialValue. Scary though it might be, here's the result:
const processCustomerCheckout = pipe( checkInventory, withShipping, withPayment, confirmOrder)Look at this beauty. No repetition, only business logic. Each building block is now a nice, testable, and reusable component.
Speaking of reusable, it is now very easy to mix and match the components as we deem appropriate:
// 1const processStorePickup = pipe( checkInventory, withPayment, confirmOrder)
// 2const generateQuote = pipe( checkInventory, createQuote)We now support two new flows. Store pickup (1) no longer requires shipping, so we omit that step, and create another pipeline. Another flow just generates quotes (2), which requires no payments or shipping, but we add a new building block for quote generation. This is just as seamless as before, yet another clean and testable pipeline.
Chasing DRY sure got us somewhere.
I'm Just Trying to Be Reasonable
I don't think that many people would find what I wrote so far particularly controversial. It's the sort of stuff that might come up in a code or design review.
Who would object to improving the maintainability of their code? Making code more testable? Great. Scratching the DRY itch? That's how I pass most of my days.
All I'm trying to do is be reasonable, and improve the code I write. The only difference between what I'm doing and a random bunch of "best practices" for code improvement is the source of these practices. Without mentioning it, every single one of them is informed by functional programming.
The fear of spooky action at a distance led us to immutability, the default in functional languages. The search for testability led us to the segregation of side-effecting code from pure code (business logic). Take this a step further and we get to "functional core, imperative shell". Lastly, the need for DRY sneakily got us into using higher-order functions (withLogging v1), partial function application (withLogging v2), and a functional combinator (pipe). All hallmarks of the functional style.
And this is just scratching the surface. Dig a bit deeper and you'll find more tools that take advantage of immutability, more techniques to control side-effects, more ways to combine functions in useful ways. And much, much more.
Applied judiciously, techniques from functional programming will lead you towards code that is easier to reason about locally12, easier to reuse, easier to refactor, easier to test, and generally easier to maintain. All relevant concerns for the plain old programmer, no academic gymnastics involved.
Yeah, but.
The Monad Elephant in the Room
It's much more common to hear about functional programming alongside intimidating academese like monads, functors, category theory, and oh my... It might seem that the typical functional programmer is stuck in an ivory tower onanistically trying to force mathematical abstractions down the CI pipeline.
I won't argue that this isn't a real phenomenon. And I can't say I'm completely innocent of doing just that myself. But it's not entirely our fault.
Once you start with the basics of untangling the spaghetti of rampant side-effects, the clarity you obtain reveals the deeper repetitive structures of your code13. And it becomes yet another thing you want to DRY. And you know who has a hundreds of years old track record of systematically discovering and categorizing repetitive structures? Mathematicians, that's who.
So it's not that surprising that some of the structures that appear in programming overlap with structures that mathematicians already discovered. Once you see it, it's hard to unsee. And that's how you rediscover monads14, and then the rest of the **** owl15. Nobody's forcing us down that path, but these abstractions are so powerful and reusable that it's a slippery slope. One that's both fun and practically useful.
So no, I'm not a functional programmer, I just happen to wield a very powerful toolbox to make code better. And you?
Footnotes
-
Pun intended. ↩
-
Although I could argue that naming things is better than not. There's a reason why classic OOP design patterns are so popular. A name is a powerful thing. ↩
-
Originally from discussions about physics. ↩
-
But will likely produce exceedingly ugly code that you will most definitely ignore, "it just tests, who cares...". ↩
-
Despite my whole blog possibly being an indication of the opposite. ↩
-
Not in the "code golf" sense, reducing the absolute number of characters is not what I'm aiming at. ↩
-
In real life you'll probably need to handle rollbacks and the like to make this thing actually transactional, but that goes way beyond the scope of this post. ↩
-
Compare with classic Template Method design pattern. ↩
-
Yeah, yeah, this is JavaScript, you can just reuse the same variable multiple times. But given what I wrote before I err on the side of using
constwhere possible. And even if not, this is just an example, so chill... ↩ -
Although functional programming is not the only way to attain local reasoning. See for example how the imperative Rust can take you in the direction of local reasoning. ↩
-
The eagle-eyed viewers amongst might've noticed that we got very close to rediscovering the
Try/Resulttype from functional programming, along with the monadic operations it supports. ↩ -
Although "monad" is not really an FP term, or even a mathematical term. It seems to have come to us from philosophy. And it appears that Microsoft had them all along. ↩
-
In case this is somehow unfamiliar, I'm referring to this wonderful meme. ↩