What if the team hates my functional code?
Indulge me a moment, and picture the following scenario. You’ve just come back to work after a summer break. And over the break, you’ve been working hard. You’ve taken some time to learn functional programming. You’ve been reading articles, devouring books, following tutorials, and practising code kata. And for some reason, this summer, something clicked. Somehow, the pieces fell into place. It makes sense. The way you think about code will never be the same again.
Now, though, you’re back at work. And you’re eager to put what you’ve learned into practice. You grab a new task from the backlog, and you’re away. It feels fantastic. It’s like the code is flowing through your fingertips straight into the computer. And you know the code you’re writing is objectively better than the code you used to write. You’re confident that it works. It has comprehensive test coverage. It’s less likely to explode, because side effects are carefully managed. It handles errors gracefully when things do go wrong. It’s clean. It’s elegant. It’s expressive. And you’re justifiably proud.
Next, you create a pull request (PR) so the team can review your work. And you’re not expecting applause. It’s just code, after all. But you’re hoping that maybe the senior might notice there’s been a change. This code is solid. And compared to how you used to write, it’s light-years ahead. Hence, you’re more than a little surprised when critical comments start trickling in.
- “This is too clever. It’s unreadable.”
- “How will we debug this?”
- “Have you measured to see if this degrades performance?”
- “Have you considered the effect of this new library on our bundle size?”
- “Too much magic going on here”
Someone even writes “how do errors get handled here?” in the exact place where you wrote an elegant abstraction to factor out the error handling. The code handles the errors fine. They just can’t see it.
And, try as you might, you can’t help but feel hurt. It’s like you’re under attack. You’re confused. The code is better than what you used to write. You’re sure of that. But they’re carrying on as if you lit a paper bag full of dog poop on fire, and asked them to merge it to the codebase. What’s going on?
Why all the hate?
To understand what’s going on, we need to step back and get a little perspective. You’re confident that the code is solid. It works. And that’s a good thing. But while correct, well-factored code is good, the senior also has other concerns. They’re thinking about more than just this PR in isolation. They’re thinking about issues like:
- Are we going to need new training modules, next time we onboard someone into the team?
- What happens if this breaks in production, and we need to fix it quickly? Can I add logging statements or set debug breakpoints to get answers, fast?
- How is this code going to perform at scale, when it rolls out to thousands of customers?
- How will we keep our team’s code structure and style consistent? We want anyone in the team to be able to jump into any part of the codebase and feel confident to get work done. Will this create an area people avoid?
- What happens when you leave? Will the rest of the team understand this code and be able to update it safely?
All these are legitimate concerns. To be fair, some might prove unfounded if the senior understood your code a little better. But they’re still things that the senior needs to think about.
At the same time, though, you can’t go back. Something in your brain has changed. You can’t write code that you know to be inferior. The way that you used to write code was more error-prone, complex, and muddled. You can’t write like that any more. But that seems to be what the senior wants… or at least, what they can cope with. You’re stuck. What do you do?
Writing for an audience
The way out of this dilemma is to understand something. If you take any kind of writing course, the first advice they give you is “understand your audience”. And, like it or not, code is no different. We write code for a human audience.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.1
We write our code to suit our audience. And that will involve some compromises. Which sounds like writing inferior code. But it doesn’t have to be that way. We can find ways to write code that make it more familiar to others, without losing our confidence. The specific approach you take will change, depending on your audience. But you can almost always find a way to make the code more familiar to others.
For example, suppose you’ve written some code like the following:
const renderNotifications = flow(
map(addReadableDate),
map(addIcon),
map(formatNotification),
map(listify),
wrapWithUl
);
If you’re familiar with flow()
and function composition, then this code wouldn’t give you too much trouble. It’s not too hard to work out what’s going on. We start with a list of notifications, and for each one, we add a readable date and an icon. We then format each item (somehow) and wrap it in an <li>
element. Finally, we wrap the list with a <ul>
.
This code has some aspects that might make our hypothetical senior nervous, though. For a start, using flow()
hides the input parameters of the function. That might not bother you or me. We can figure out what’s going on because we know what flow()
does. But for someone not familiar with flow()
, this can be confusing. And we can make a small change that will help them feel more comfortable:
const renderNotifications = (notifications) => pipe(
notifications,
map(addReadableDate),
map(addIcon),
map(formatNotification),
map(listify),
wrapWithUl
);
If we switch from flow()
to pipe()
, most of the code stays the same. But we’ve reintroduced the input parameter. And this might help people who aren’t used to flow()
.
For many people, though, using pipe()
and flow()
at all can be a problem. They might simply have never come across this kind of thing before and not understand how it works. But, perhaps they’re okay with calling methods on objects. We can rewrite our function to use chained method calls:
const renderNotifications = (notifications) =>
wrapWithUl(
notifications
.map(addReadableDate)
.map(addIcon)
.map(formatNotification)
.map(listify)
);
If notifications
is an array, then we can switch to calling .map()
and chain the method calls. But, the wrapWithUl()
function is a bit of a problem. It’s not built-in to the array prototype, so we have to wrap the function call around the whole thing. This makes the order less clear. We can fix that, though, by adding an interstitial variable:
const renderNotifications = (notifications) => {
const renderedItems = notifications
.map(addReadableDate)
.map(addIcon)
.map(formatNotification)
.map(listify);
return wrapWithUl(renderedItems);
};
Now, it’s even possible that some people might find these chained method calls too much. In that case, we can add an interstitial variable for each line:
const renderNotifications = (notifications) => {
const withDates = notifications.map(addReadableDate);
const withIcons = withDates.map(addIcon);
const formattedItems = withIcons.map(formatNotification);
const listItems = formatedItems.map(listify);
return wrapWithUl(listItems);
};
We’ve now written this code five different ways. But notice what we haven’t done. We haven’t introduced any side effects. We’re still working with pure functions that each do one small thing. We haven’t added any shared mutable state. And because we haven’t done these things, we can still be confident about our code. It’s still easy to test. It’s still ‘functional.’
Someone might be wondering, though, why not jump straight to the fourth or fifth version then? Why not make the code as readable as possible for everyone?
That’s a reasonable question. In fact, it’s more than reasonable, it’s admirable. It’s good to make things accessible. But, again, it comes down to the purpose and audience you’re writing for. We don’t start a physics paper with a recap of the laws of thermodynamics. If you’re writing an academic paper in physics, you expect the audience to know those. And to include them would be tedious for the readers. It would make their life harder, not easier. And the same thing goes for writing code. Different audiences will prefer different styles. And different people will need help with different aspects of the code.
Beyond this, there are some advantages to the function composition style. (As in our first and second examples). When we work with function composition, it changes the way we think about our code. It opens up different possibilities for how we combine and re-use code. If the team gets it, we want to use that style to open up those possibilities. But they don’t always get it. And that’s okay. We can still keep the core advantages of functional programming. We can write code and be confident that it works. And with some luck, the rest of the team will start to catch on in time.
P.S.: If you’ve ever experienced anything like this before, I’m written a book that may help. It’s called A Skeptic’s Guide to Functional Programming with JavaScript.