When I first heard about Functional Programming, I thought it was about using functions. If it was about using functions, I was fine with it; I used them.
A couple of years ago, I was chosen to talk about programming paradigms in our trainee program. To gain perspective, I dug into the FP paradigm, and there’s much more than ‘just use functions’ or ‘don’t reassign a variable’. There’s much math theory surrounding it.
Knowing the theory is good, but you don’t need to—at least, you don’t need to have a deep knowledge about it.
In this article, I’ll present three or more Functional Programming concepts. The good news. This article was made especially for people who don’t know much about the FP paradigm. However, it’s still useful for any developer who wants to write better code.
Cover image from https://www.deviantart.com/aristarchusnull/art/Functional-Programming-Wallpaper-1-951473693
The Enough Theory (for now)
We can’t start talking about Functional Programming without mentioning Lambda Calculus. It’s the foundation. It’s where many FP programming languages like Haskell and Clojure bring their inspiration from.
Lambda Calculus is a mathematical system that defines functions and the application of functions. It’s simple, stateless yet powerful. Anything can be computed with Lambda Calculus. You just need:
- a function: represented by
λ
taking only one argument and returning an expression; - parentheses: to group expressions;
It’s as simple as that. You might be wondering how it’s possible to compute anything with such a simple system. Demonstrating this is not the goal of this article, but I will show you a simple example to illustrate how powerful it is.
Lambda Calculus is so powerful that we can redefine some constructions present in programming languages. Constructions like booleans
and if
statements can be defined using only functions.
NOTE: we’re using JavaScript which is unable to interpret lambda expresions. Functions will be used.
// λa.λb.a
function TRUE(a) {
return function(b) {
return a;
};
}
// λa.λb.b
function FALSE(a) {
return function(b) {
return b;
};
}
// λc. λa. λb. (c a b)
function IF(c) {
return function(a) {
return function(b) {
return c(a)(b);
};
};
}
With these definitions, we can now use IF
to define a conditional statement.
const ifResult = IF(TRUE)('It it true')('It is false');
console.log(ifResult); // It is true
Note in this case we turned the if
into an expression that returns whichever branch is evaluated and we just used
functions that take one argument.
Not Simple Enough?
I know that at first it might seem a little bit confusing, but bear with me. Looking at the definitions of TRUE
, FALSE
, and IF
you’ll see that they are simple to reason about.
TRUE
is a function that takes two arguments and returns the first one;FALSE
is a function that takes two arguments and returns the second one;IF
is a function that takes three arguments and returns the result of the first argument if it’sTRUE
or the second argument if it’sFALSE
.
Of course, I’m mentioning that functions take two or three arguments, but in reality, they take only one argument and return another function that takes another argument. This is called Currying
. Any function that takes n
arguments can be turned into a sequence of functions that take only one argument. Look at the next example:
function add(a, b) {
return a + b;
}
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
add(1, 2); // 3
curriedAdd(1)(2); // 3
Note how a function that takes two arguments can be turned into a sequence of two functions that take only one argument.
Currying is a useful concept, it has a couple of applications, but they are out of the scope of this article.
Pure Functions
The definition of a pure function is simple:
- It’s a function that given the same input will always return the same output (no variation with local static variables, non-local variables, mutable reference arguments or input streams, i.e., referential transparency);
- It has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments, or input/output streams).
From this definition, you can see that TRUE
, for example, is a pure function. It doesn’t depend on any external state to return the same result. It’s always the same.
On the other hand, IF
might be impure. It depends on the arguments – in this case, functions – to return a result. If the given functions have side effects, then IF
will have side effects as well.
Don’t Fear Impurity, Handle It
Purity is something people tend to focus on when learning FP. Though it’s important and useful, it’s just a concept. A software to be functional needs side-effects. You don’t want a mailing system that doesn’t send emails, right?
You should then isolate the impure parts of your code and make them as small as possible. Eric Normand, in his book Groking Simplicity
, states three main components of useful software:
- data: the information you are working with;
- calculation: the transformation of data;
- actions: the side effects.
You should isolate the actions from the data and calculations. This way, you can reason about your code more easily and test it more effectively.
Rick is a software engineer who works for a company that sells products online. He’s working on a feature that sends an email to the customer when the product is shipped. He wants to apply the concepts he learned about FP to make his code more reliable and easier to test.
function composeEmail(product) {
return {
to: product.customer.email,
subject: 'Your product has been shipped',
body: `Your product ${product.name} has been shipped. It will arrive in ${product.deliveryTime} days.`
};
}
async function sendEmail(composedEmail) {
const { to, subject, body } = composedEmail;
await gmail.send(to, subject, body); // side-effect
}
async function shipProduct(product) {
const composedEmail = composeEmail(product);
await sendEmail(composedEmail);
}
Even though we don’t have domain rules in this example, it’s possible to see how Rick’s approach isolates what matters from the details (or actions). Note how we can use any provider to send the email without touching the function that composes the email.
This concept is mandatory in Domain-Driven Design (DDD) and Architectural Patterns. Knowing how to isolate things will bring you and your team many benefits.
Composition
Inheritance vs Composition
is a common topic in Object-Oriented Programming (OOP). You probably heard about it and the Composition over Inheritance
principle.
The inheritance issue is that it’s an is-a
relationship. It’s good for taxonomy. For instance, a Duck
is a Bird
so it makes sense to use inheritance. An issue, with this approach, might arise when the parent class has behaviors defined that will dictate the child’s class behavior.
Suppose in the class Bird
we have a method fly
. It’s a good method for a Bird
since most birds can fly. But Penguin
s do not fly. If we use inheritance, we have to override the fly
method in the Penguin
class.
A more clever approach is to isolate the behaviors and use what you need where you need, isolating what changes from what doesn’t.
The characteristics of a Bird
that are common to all birds can be in a Bird
class. The behaviors that are specific to children’s classes can be isolated and composed where needed. For instance, we can have a Flyable
interface that has the fly
method and compose it where needed.
NOTE: There are other approaches to address this problem.
Composition in FP
The essence of composition is the same as discussed in the last section: compose small pieces to build something new, often, with different traits. In FP, composition is closer to what you learned in Mathematics. You have a function f
and a function g
. Composing two functions is applying the result of one function to the other. For instance, if you have f(x) = x + 1
and g(x) = x * 2
, then f(g(x)) = (x * 2) + 1
– read as f after g
. This can represent a new function h(x) = (x * 2) + 1 = f(g(x))
.
But we can have a different perspective. Take our code that composes and sends emails. Note that the shipProduct
is itself a composition. It’s easier to see that if we rewrite the function as:
async function shipProduct(product) {
await sendEmail(composeEmail(product));
}
Let’s catalogue things. In the function above we have:
product
: the data containing product information;a composed email
: the result of the calculation of composing the email;send email result
: the action result of sending the email.
To make it simple, let’s name these three objects A, B, and C respectively. Functions, in this example, are just mappings between these objects. The function composeEmail
maps A to B. The function sendEmail
maps B to C. The function shipProduct
is a composition of these two functions, mapping A to C.
Flying Within A Category
Analogously, you can also think of Bird
, Walkable Bird
, and Flyable Bird
as objects. With morphisms or arrows – the functions – a bird can be turned into a duck or a penguin with their specific behaviors. The behaviors are the morphisms that compose the objects. See the example below:
function makeBirdFly(bird) {
return {
...bird,
fly: () => 'I believe I can fly'
};
}
function makeBirdWalk(bird) {
return {
...bird,
walk: () => 'I can walk'
};
}
const duck = makeBirdFly(makeBirdWalk({ name: 'Duck' })); // { name: 'Duck', fly: [Function], walk: [Function] }
const penguin = makeBirdWalk({ name: 'Penguin' }); // { name: 'Penguin', walk: [Function] }
These concepts come from Category Theory. A branch of Mathematics that studies categories, objects, and morphisms. One more concept in Category Theory is the identity morphism
or identity function. It’s a function that doesn’t change the object. In our example, it would be a function that doesn’t change the bird. It’s a function that takes an object and returns the same object.
function identity(object) {
return object;
}
I know it sounds useless but it’s like zero in sum operations or one in multiplication. Identities are very important in Category Theory. That’s what allows monads to exist.
Immutability
You just learned a bit about Category Theory and Lambda Calculus in a practical way. Because that’s the thing. You want to use concepts to build something useful. You are a programmer, not a mathematician.
In none of the previous examples, a mutable state was needed. This is amazing. The code is clear. Easy to change and test. Functions are the only constructions you need. Even classical constructions like loops
can be turned into functions.
for (let i = 0; i < 5; i++) {
console.log(i);
}
How can we reach the same behavior without mutating the i
variable? The answer: with functions.
function loop(initialValue, condition, increment, action) {
if (condition(initialValue)) {
action(initialValue);
loop(increment(initialValue), condition, increment, action);
}
}
loop(
0,
(i) => i < 5,
(i) => i + 1,
(i) => console.log(i)
);
Yes, this is recursion. It allows you to iterate without having to mutate variables. Instead, you call the function again with the new value. In many FP languages that’s the way to loop. Drawing the calls it would be something like
loop(
0,
(i) => i < 5,
(i) => i + 1,
(i) => console.log(i)
);
loop(
1,
...,
...,
...
);
loop(
2,
...,
...,
...
);
...
loop(
5,
...,
...,
...
);
Recursion is a very nice tool to keep stuff immutable but still working as if we were mutating them.
Be Careful With Immutability In Non-FP Languages
Despite writing immutable code is a good thing, it’s not always possible. Immutability in a programming language like Erlang or Elixir is possible because the language was designed to work that way. JavaScript is not meant to be like those languages.
Sometimes you will need to write imperative code to leverage performance and that’s fine. Don’t be afraid of using for
loops if you need to. Don’t be afraid of reassigning a variable a new value. Just analyze the trade-offs and pick the best approach for your case.
How Does This Make Your Code Better?
Couldn’t see it yet? Let me summarize it for you.
Pure Functions
Writing pure functions makes your code more predictive and simpler to test. Calculations are the main components you want to test. They will have some logic involved.
Since there’s no side-effect no excessive mocking is needed.
Dependency Injection made up of this.
Immutability
You won’t get in trouble with a function unexpectedly changing a shared state that makes your app crash or behave incorrectly.
Immutability simplifies concurrency. If your function is self-contained you can easily work it using multiple threads or processes. That’s why good players at concurrency like Erlang and Elixir have this feature.
Composition
With composition, you isolate what matters from what doesn’t. That’s the essence of DDD and Architectural Patterns. With proper isolation, you can – again – write better tests and switch application components that are not your business logic.
It might happen. Discord, for instance, changed from MongoDB to Cassandra and from Cassandra to ScyllaDB.
Composition is strong even in OOP. In most cases, it's better to isolate the behaviors and compose them with what you need.
Conclusion
With the right tools you can build something reliable and beautiful. Enjoy your new FP toys!
See ya!
We want to work with you. Check out our "What We Do" section!