Can You Avoid Functional Programming as a Policy?
Functional programming has penetrated most of the mainstream programming world — much of the JS ecosystem, Linq for C#, even higher-order functions in Java. This is Java in 2018:
getUserName(users, user -> user.getUserName());
FP is so useful and convenient, it has found its way into every modern programming language in mainstream use that I can think of.
But it’s not all rainbows and puppies. Many developers are struggling with this tectonic shift in how we think about software. Frankly, it’s hard to find a JavaScript job today that doesn’t require some comfort with functional programming concepts.
FP lies at the heart of both of the dominant frameworks, React (avoiding shared mutable DOM is the motivation for its architecture and 1-way data flow) and Angular (RxJS is a library of utility operators that act on streams by way of higher-order functions — its used extensively throughout Angular). Redux and ngrx/store as well: functional on both counts.
Functional programming can be an intimidating topic to approach for developers who are new to it, and in the interest of quick onboarding, somebody on your team might suggest you make it easier for your team to adapt to the codebase by avoiding FP as a policy.
For managers unfamiliar with what FP is, or its pervasiveness in the modern code ecosystem, that might actually sound like a sane suggestion. After all, didn’t OOP serve the software world well for for 30 years? Why not keep doing that?
Let’s entertain that notion for a moment.
What would it even mean to “ban” FP as a policy?
What is functional programming?
My favorite definition of functional programming is this:
Functional programming is a programming paradigm using pure functions as the atomic unit of composition, avoiding shared mutable state and side-effects.
A pure function is a function which:
- Given the same input, always return the same output
- Has no side effects
The essence of FP really boils down to:
- Program with functions
- Avoid shared mutable state & side effects
When you put those together, you get software development using pure functions as your building blocks (the “atomic unit of composition”).
Incidentally, according to Alan Kay, the instigator for all modern OOP, the essence of OOP is:
- Encapsulation
- Message Passing
So OOP is just another approach to avoiding shared mutable state and side-effects.
Clearly, the opposite of FP is not OOP. The opposite of FP is unstructured, procedural programming.
Smalltalk (where Alan Kay laid the foundations of OOP) is both object-oriented and functional, and the idea of picking one or the other is completely foreign and unthinkable.
The same is true of JavaScript. When Brendan Eich was hired to build JavaScript, the ideas were:
- Scheme in the browser (FP)
- Look like Java (OOP)
In JavaScript, you can try to favor one or the other, but for better or worse, they’re inextricably linked. Encapsulation in JavaScript relies on closures — a concept from functional programming.
Chances are very good you’re already doing some functional programming whether you know it or not.
How to NOT do FP:
In order to avoid functional programming, you’d have to avoid using pure functions. The problem with that is, now you can’t write this because it might be pure:
const getName = obj => obj.name;
const name = getName({ uid: '123', name: 'Banksy' }); // Banksy
Let’s refactor that so it’s not FP anymore. We could make it a class with a public property, instead. Since it’s not using encapsulation, it’s a stretch to call this OOP. Maybe we should call this “procedural object-programming”?:
class User {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}const myUser = new User({ uid: '123', name: 'Banksy' });
const name = myUser.getName(); // Banksy
Congratulations! We’ve just turned 2 lines of code into 11 and introduced the probability of uncontrolled, external mutation. What have we gained?
Well, nothing, really. In fact, we’ve lost a lot of flexibility and the potential for significant code savings.
The previous getName()
function worked with any input object. This one does, too (because it’s JavaScript and we can delegate methods to random objects), but it’s a lot more awkward. We could let the two classes inherit from a common base-class — but that implies relationships that may not need to exist between object classes at all.
Forget reusability. We’ve just flushed it down the drain. Here’s what code duplication looks like:
class Friend {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}
A helpful student chimes in from the back of the room:
“Create a person class, of course!”
But then:
class Country {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}
“But obviously these are different types. Of course you can’t use a person method on a country!”
To which I answer: “Why not?”
One of the amazing benefits of functional programing is trivially easy generic programming. You might call it “generic by default”: The ability to write a single function that works with any type that satisfies its generic requirements.
Note: For the Java people, this isn’t about static types. Some FP languages have great static type systems, and still share this benefit using features like structural and/or higher-kinded types.
This example is trivial, but the savings in code volume we get from that trick is monumental.
It enables libraries like autodux to automatically generate domain logic for any object made up of getter/setter pairs (and a lot more, besides). This trick alone can cut the code for your domain logic in half or better.
No more higher order functions
Since most (but not all) higher-order-functions take advantage of pure functions to return the same values based on the same inputs, you also can’t use functions like .map()
, .filter()
, reduce()
without throwing in a side-effect, just to say you’re not pure:
const arr = [1,2,3];const double = n => n * 2;
const doubledArr = arr.map(double);
Becomes:
const arr = [1,2,3];const double = (n, i) => {
console.log('Random side-effect for no reason.');
console.log(`Oh, I know, we could directly save the output
to the database and tightly couple our domain logic to our I/O. That'll be fine. Nobody else will need to multiply by 2, right?`);
saveToDB(i, n);
return n * 2;
};const doubledArr = arr.map(double);
RIP, Function Composition 1958–2018
Forget about point-free composition of Higher Order Components to encapsulate your cross-cutting concerns across pages. This convenient, declarative syntax is now off-limits:
const wrapEveryPage = compose(
withRedux,
withEnv,
withLoader,
withTheme,
withLayout,
withFeatures({ initialFeatures })
);
You’ll have to manually import each of these into every component, or worse — descend into a tangled, inflexible class-inheritance hierarchy (rightly considered an anti-pattern by most of the sane world, even [especially?] by OO design canon).
Farewell, promises & async/await
Promises are monads. Technically from category theory, but I hear they’re an FP thing, too ’cause Haskell has them and uses monads to make everything pure and lazy.
Honestly, losing monad and functor tutorials is probably a good thing. These things are so much easier than we make them sound! There’s a reason I teach people how to use Array.prototype.map
and promises before I introduce the general concepts of functors and monads.
Do you know how to use those? You’re more than half way to understanding functors and monads.
So, to avoid FP
- Don’t use any of JavaScript’s most popular frameworks or libraries (they will all betray you to FP!).
- Don’t write pure functions.
- Don’t use lots of JavaScript’s built-in features: Most of the Math functions (because they’re pure), non-mutating string & array methods, .map(), .filter(), .forEach(), promises, or async/await.
- Do write unnecessary classes.
- Double (or more) your domain logic by manually writing getters and setters for literally everything.
- Take the “readable, explicit” imperative approach and tangle your domain logic with rendering and network I/O concerns.
And say goodbye to:
- Time travel debugging
- Easy undo/redo feature development
- Rock solid, consistent unit tests
- Mock and D/I free testing
- Fast unit tests with no dependencies on network I/O
- Small, testable, debuggable, maintainable codebases
Avoid functional programming as a policy? No problem.
Oh, wait.
Learn More at EricElliottJS.com
Video lessons on functional programming are available for members of EricElliottJS.com. If you’re not a member, sign up today.
Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), and cofounder of the software mentorship platform, DevAnywhere.io. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He works remote from anywhere with the most beautiful woman in the world.