Unleash JavaScript's Potential with Functional Programming
Too many JavaScript developers have no idea what JavaScript is capable of.
This article will transform the way you write code and unleash your potential as a programmer.
By the end of this article, you will be able to read, understand and write code like this:
JavaScript is a two paradigm programming language because it supports OOP and FP.
This article is your "step-by-step with no steps skipped" guide to functional programming, so you can use the language to its full capabilities and write more modular, deterministic and testable code.
It literally goes from primitives all the way to advanced function composition.
Definition
If you look up functional programming you might get lost in academic lingo.
If you're lucky you will find some kind of simple definition like this:
“Functional programming is the process of building software by composing functions. Functional programming is declarative rather than imperative. Side-effects are isolated. And application state usually flows through pure functions.”
You can relax because this article will explain all of these terms and more. Starting at Primitives.
Primitives
Primitives are all datatypes that can hold only one value at a time. JavaScript has 7 primitive data types.
string
: Represents text. (ReactSquad plug)number
: Represents numerical values.boolean
: Represents true or false.undefined
: Indicates a variable has not been assigned a value.null
: Represents an intentional absence of any object value.Symbol
: Creates unique identifiers.BigInt
: Handles numbers larger than the standardnumber
type can.
Composite Data Types
Composite data types can store collections or combinations of values.
- Object: A structure that can hold multiple values as named properties.
- Array: A list-like structure that holds values in a specific order.
- Map: A collection of keyed data items, similar to an object but with keys of any type and maintains the order of insertions.
- Set: A collection of unique values. Like an array, but each value can only occur once.
A Map
in JavaScript is a collection of keyed data items that maintains the order of insertions and allows keys of any type.
A Set
in JavaScript is a collection of unique values, ensuring that each value appears only once.
Functions
A function is a process that can take inputs, called parameters, and can produce some output called return value.
Parameters vs. Arguments
- Parameters are variables defined in the function declaration. They act as placeholders for the values that a function will operate on.
- Arguments are the actual values or data passed to the function when it is called. These values replace the parameters during the function's execution.
- An application happens when the arguments are used to replace the function's parameters. This allows the function to perform its task using the provided arguments.
A function can be a mapping, a procedure or handle I/O operations.
- Mapping: Produce some output based on given inputs. A function maps input values to output values. (So the examples you saw earlier are mappings.)
- Procedure: A function executes some steps in a sequence. The sequence is known as a procedure, and programming in this style is known as procedural programming.
- I/O operations: I/O is short for input and output. Functions may handle interactions with system components like the screen, storage, logs, or network.
Methods
Methods are functions attached to objects. They allow you to perform operations on the object's data. In JavaScript, methods are commonly used with objects, arrays, and other built-in types.
Earlier in this article, you saw how to use methods of the Map
and Set
objects.
Primitives also have methods because under the hood everything in JavaScript is an object.
Noop
A "noop" stands for "no operation." It describes a function, operation, or command that does nothing. This can be useful for placeholders in code or to intentionally have no effect.
In JavaScript, a noop
function looks like this:
Pure Functions
A function is a pure function, if:
- given the same inputs, it always returns the same output, and
- it has no side-effects.
You've seen pure functions already. For example, both sum
functions and the square
function are pure.
Pure functions are deterministic. This is captured by the first property. A pure function will always produce the same output for the same set of inputs, no matter when or how many times it is called. This predictability is a key property of pure functions and is essential for reliable and testable code.
Here is an example for a function that violates the first rule.
generateRandomInt
can be called with the same start
and end
values, but it produces different results because it uses Math.random()
.
And here is a example, which violates the second rule.
Even though sideEffectingFunction
returns something, it pushes code as a side-effect to an array and you can meaningfully call it without using it's return value.
“A dead giveaway that a function is impure is if it makes sense to call it without using its return value. For pure functions, that's a noop.” - Eric Elliott
Idempotence
Another concept you need to know is idempotence.
Idempotence is a property of certain operations in which no matter how many times you perform the operation, the result remains the same after the first application. For example, setting a value to 5 is idempotent because no matter how often you do it, the value remains 5.
All pure functions are idempotent, but not all idempotent functions are pure functions.
An idempotent function can cause idempotent side-effects.
A pure function cannot.
Deleting a record in a database by ID is idempotent, because the row of the table stays deleted after subsequent calls. Additional calls do nothing.
Here is a synchronous example.
Referential Transparency
Idempotent functions without side-effects have a feature known as referential transparency.
That means that if you have a function call:
You could replace that function call with the result of square(7)
without changing the meaning of the program. So, for example if the result of square(7)
is 49
. Therefore, you could change the code above to:
and your program would still work the same.
Why Do You Need Functions?
After reading the former part, a mentee asked me:
“Why do you need functions? I'm trying to understand why and when to use functions versus objects in programming. I'm curious if theoretically everything could be done without functions, just by manipulating data in variables. I realize it might be more cumbersome to handle everything with variable declarations, but I'm interested in understanding the core value or use case of functions.”
You use functions because they flexibly describe data points.
Suppose you want to know the orbit of a planet in the past.
You could either memorize all human archives.
Or you learn just the formula, which is the function in the context of programming. The formula is easier to remember. But sometimes, executing the formula is more difficult than looking up archived data.
The real advantage is that you can use the formula for infinitely different applications. For example, to predict future orbits or to correct errors in the archives.
Functional Programming Prerequisites
A language needs three features to support functional programming, and JavaScript has all three:
- First-class functions (and therefore higher-order functions),
- closures, and
- anonymous functions and concise lambda syntax.
First-Class Functions
In JavaScript, functions are treated as first-class citizens. This means that functions can be stored in variables. Therefore, you can use functions as:
- arguments to other functions,
- return values from functions,
- values to an object’s keys.
If you want to learn what first-class functions enable in React check out this article about higher-order components.
Higher-Order Functions
The last example multiply
that you saw is called a "higher-order function" because when you call it for the first time with a number, it returns a function.
Any function that takes in or returns a function is called a higher order function.
Closure
The multiply
function from earlier hid another key concept: Closure.
A closure happens when a function is bundled together with it's lexical scope. In other words, a closure gives you access to an outer function’s scope from an inner function. Closures in JavaScript are created whenever a function is created, at function creation time.
To use a closure, define a function inside another function and expose it by returning the inner function or passing it to another function.
The inner function can access variables in the outer function's scope, even after the outer function has returned.
Closures serve three purposes:
- They provide data privacy for objects.
- In functional programming, they enable partial application and currying.
- They act as parameters for callback or higher-order functions like
map
,reduce
, andfilter
.
You're going to see 2.) and 3.) later in this article, so let's take a look at the concept of data privacy.
No external influence can manipulate the count
value of counter1
. You need to call counter1
to increment it.
This matters because some applications require private state. A common pattern is to prefix the private key with __
.
However, junior developers might not know that __
signals: "Do NOT change this key.", so they mutate it. And senior developers sometimes think it's okay to change it because they believe they know better.
Closures give you a reliable way to enforce data privacy for everyone.
Imperative vs. Declarative
As you learned at the start of this article, functional programming is declarative. But what does that mean?
Imperative code describes "how" to do things. The code contains the specific steps needed to achieve your desired result.
Declarative code describes "what" to do. The "how" gets abstracted away.
In other words, imperative programming is about defining the process to reach an outcome. This is known as flow control, where you dictate each step of the computation.
Declarative programming, on the other hand, is about defining the outcome, known as data flow. Here, you describe what you want, and the system determines how to achieve it.
Here is an example for imperative code.
And here is another example of imperative code, but this time with a custom function.
Before you look at declarative code, you need to understand immutability and abstraction.
Immutability
Immutability in programming means that an object or value cannot be modified after it is created; instead, any changes result in a new object or value.
Similarly, mutable state is state that can be modified after you created it.
Immutability is a central concept of functional programming because with it, the data flow in your program is preserved. State history is maintained, and it helps prevent strange bugs from creeping into your software.
“non-determinism = parallel processing + mutable state” - Martin Odersky
You want determinism to make your programs easy to reason about, and parallel processing to keep your apps performant, so you have to get rid of mutable state.
Generally speaking, when you write your code using functional programming it becomes more deterministic, easier to reason about, easier to maintain, more modular and more testable.
Abstraction
Abstraction is a fundamental concept in programming that involves hiding the complex reality while exposing only the necessary parts of an object or a system.
There are two types of abstraction: generalization and specialization.
Generalization is when you create a more universal form of something for use in multiple contexts. This process identifies common features among different components and develops a single model to represent all variations.
This is what most people think of when they hear "abstraction" and what Eric Elliott refers to when he says:
“Junior developers think they have to write a lot of code to produce a lot of value.
Senior developers understand the value of the code that nobody needed to write.” - Eric Elliott
Specialization is when you apply the abstraction to a specific use-case and add what makes the current situation different.
The hard part is knowing when to generalize and when to specialize. Unfortunately, there is no good rule of thumb for this - you develop a feel for both with experience.
And what you're going to find is:
Abstraction is the key to simplicity.
“Simplicity is about
subtracting the obvious
and adding the meaningful.” - John Maeda
Using the functional programming paradigm you can create the most beautiful abstractions.
At this point you're probably starving for examples and you're going to see some soon.
Now, it's time to look at declarative code, which will also show you more immutability and some abstraction.
Array methods
Remember, declarative programming is about "what" to do.
The perfect example of declarative code in JavaScript are the native array methods. You're going to see the three most common ones: map
, filter
and reduce
.
map
Take a look at the map
function first. It does exactly what we did earlier with the for
loop, but the code is a lot more concise.
The map
method is a perfect example of abstraction.
map
removes the obvious: iterating over the array and changing each value. Since it takes in a function, the map
method is a higher-order function.
You only supply the meaningful: the function that doubles a number, which map
applies to every number in the array. This double
function is an anonymous function using the concise lamda syntax.
In general, a concise lambda is a simplified way to write a function with minimal syntax. In JavaScript it refers to the arrow function syntax.
map
is also immutable because it returns a new array. numbers
and doubled
are two distinct arrays and the numbers
array still contains the numbers 1 through 5. You can verify this by mapping using the identity
function that returns its input.
Even though numbers
and clone
are both an array with the numbers 1 through 5, they are different array instances.
You might be asking yourself: "Isn't that generating tons of data that no one uses?"
Well kinda, but compared to what modern laptops are capable of the data amount is tiny, and JavaScript has garbage collection which clears up the stale memory.
filter
The filter
method takes in a special function called the "predicate". A predicate is a function that always returns only a boolean. It tests each element in the array. If it returns true
, the element is included in the resulting array.
filter
also returns a new array aka. it's immutable.
reduce
The reduce
method in JavaScript processes an array to produce a single output value. It takes a reducer function and an optional initial value. The reducer function itself accepts two parameters: an accumulator
(which holds the accumulated result) and the currentValue
from the array. The reduce
method is also immutable.
Here is an example where you can use reduce
on an array of numbers from 1 to 4, summing them together.
In this example, reduce
is called on the numbers
array. The sumReducer
function is used to add each number to a running total, starting from 0. Here's what happens at each step:
Step | Accumulator | Current Value | Operation | New Accumulator Value |
---|---|---|---|---|
1 | 0 | 1 | 0 + 1 | 1 |
2 | 1 | 2 | 1 + 2 | 3 |
3 | 3 | 3 | 3 + 3 | 6 |
4 | 6 | 4 | 6 + 4 | 10 |
Details of the Process:
- Step 1: The accumulator starts at 0 (the initial value). The current value is the first element of the array, which is 1. They are added together to make the new accumulator value 1.
- Step 2: The accumulator is now 1. The current value is the next element in the array, 2. Adding these gives 3.
- Step 3: The accumulator is now 3, and the current value is 3. Adding these gives 6.
- Step 4: The accumulator is now 6, and the current value is the last element, 4. Adding these gives the final result of 10.
At the end of the process, the reduce
method returns 10, which is the sum of all elements in the array. This demonstrates how reduce
can be used to transform an array into a single value through repeated application of a function.
reduce
is the most powerful method because you can implement map
and filter
with reduce
, but neither filter
nor reduce
with map
and neither map
or reduce
with filter
.
You can implement map
with reduce
like this:
You can implement filter
with reduce
like this:
Expressions Over Statements
In functional programming, you'll see many expressions and few statements. Expressions avoid intermediate variables, while statements often bring side-effects and mutable state.
Statements
Imperative code frequently utilizes statements. A statement is a piece of code that performs an action.
- Loops -
for
,while
, etc.
- Control flow -
if
,switch
, etc.
- Error handling -
try...catch
,throw
, etc.
Except for functions, if it's a keyword with curly braces, you're likely dealing with a statement. (❗️)
Expressions
Declarative code favours expressions. An expression evaluates to a value.
- Literal expressions
- Arithmetic expressions
- Logical expressions
- Function expressions
- Object and array initializers
- Property access expressions
- Function calls
Function composition
You're about to unlock a new understanding of code and gain superpowers, so "lock-in".
“All software development is composition: The act of breaking a complex problem down to smaller parts, and then composing those smaller solutions together to form your application.” - Eric Elliot
Whenever you use functions together, you're "composing" them.
But with anything in life, you can do it better if you do it consciously. The code above is actually NOT the ideal way to write it because:
The more code you write, the higher the surface area for bugs to hide in.
less code = less surface area for bugs = less bugs
The obvious exception is clear naming and documentation. It's fine if you give a function a longer name and supply doc strings to make it easier for your readers to understand your code.
You can reduce the surface area for bugs by avoiding the capturing of the intermediary results in variables.
In mathematics, function composition is taking two functions f
and g
and applying one function to the result of another function: h(x) = (f ∘ g)(x) = f(g(x))
. Note: The hollow dot ∘
is called the composition operator.
In mathematical notation, if you have two functions f
and g
, and their composition is written as (f ∘ g)(x)
, this means you first apply g
to x
and then apply f
to the result of g(x)
. For your example, f(n) = n + 1
and g(n) = 2n
, the composition h(n) = f(g(n))
calculates 2n + 1
.
Note: Generally mathematicians use x
(or y
or z
etc.) to represent any variable, but in the code example above n
is used to subtly hint at the parameter to be a number. The different name has no impact on the result.
You can abstract away the composition operator ∘
into a function called compose2
which takes two functions and composes them in mathematical order.
compose2
only works for two functions at a time.
But get ready, because this is where it gets powerful.
If you want to compose an arbitrary amount of functions, you can write a generalized compose
.
The compose function here is written using the mathematical variable names. If you want to take the names that you might be used to from reduce
, then you'd write it like this:
compose
takes multiple functions as its arguments, and collects them into an array fns
via the rest syntax.
It then returns a child function that takes in the initialValue x
and returns the array fns
with the reduceRight
method applied to it. The reduceRight
method then takes in a callback function and the initialValue x
.
That callback function is the heart of compose
. It takes in the accumulator y
(starting from x
) and the currentValue f
, which is a function from the array fns
. It then returns that function f
- called with the accumulator y
.
The result of that function call, then becomes the accumulator y
for the next iteration. Here the next function f
from fns
get called with that new accumulator value. This repeats until the initialValue x
has been passed and transformed through all functions from the array fns
.
Function Call | Accumulator y | currentValue f | Operation | New Accumulator |
---|---|---|---|---|
Initial Value | 3 | - | - | 3 |
increment | 3 | increment | increment(3) = 3 + 1 | 4 |
double | 4 | double | double(4) = 4 * 2 | 8 |
square | 8 | square | square(8) = 8 * 8 | 64 |
Many people who are used to reading texts from left to right find it unintuitive to compose functions in mathematical order. Many functional programming packages provide another function called commonly pipe
, which composes function from left to right in reverse mathematical order.
trace
You might be asking right now. "But way, what if you want to debug your code? Then you need to capture the intermediate results in variables, right?"
You actually do not. You only need a helper higher-order function called trace
.
And here is how you can use it.
Currying
You saw another technique being used called currying.
Currying is a transformation of functions that translates a function from callable as f(a, b, c)
into callable as f(a)(b)(c)
. In other words, a function is curried if it can take in each of it's parameters one at a time.
With arrow functions, these definitions can become one-liners by leveraging their implicit returns.
Here a addCurried
is a function that takes in a number a
and returns a function b => a + b
. You can read it like this:
You can curry any function. For example, you can create custom map
and reduce
functions.
Map takes in two parameters and reduce takes in three. The number of parameters a function expects in its definition is called arity.
There are shorthand terms for functions that take in 1, 2 and 3 parameters.
- Unary function: Takes one argument, e.g.
square
. - Binary function: Takes two arguments, e.g.
map
. - Ternary function: Takes three arguments, e.g.
reduce
.
In the context of currying, understanding arity is important because each step in a curried function reduces the arity by one until all expected arguments have been received and the final operation can be performed.
Additionally, the first function of a composition can have any arity, but ever following function needs to be unary.
Exercise: create your own custom filter
function that is curried and takes in a predicate pred
and then an array arr
and then filters the array based on the predicate.
Wouldn't it be nice if you had a function that can curry any function?
Well there it is.
The previous example used the mathematical names for the variables .If you want to name the variables more descriptively to understand curry
now, you can write the function like this:
curry
uses recursion, which is when a function calls itself for the purpose of iteration. Let's break it down:
f
ortargetFunction
: The original function you want to curry.array
orcollectedArguments
: An array to collect the arguments of all currying calls. Each cycle (except for the last) new arguments get added.args
orcurrentArguments
: An array of the arguments taken in with the current invocation of the curried function (in this case:addCurried
).a
orallArguments
: An array of arguments concatenated from thecurrentArguments
and thecollectedArguments
from the previous calls.
When we call addCurried
with one or more arguments, these current arguments get taken in as args
in the child function of curry
. This child function returns another function (grand-child function of curry
).
The grand-child function immediately invokes itself with an array concatenated from the collected arguments array
and the current arguments args
, receiving these values from its grand-parent and parent closure. It takes this new array as the all arguments a
and checks if the all arguments a
contains the same or higher amount of arguments as the target function f
is declared with.
If yes, this means all necessary currying calls have been performed: It returns a final call of the target function f
invoked with the all arguments a
. If not, it recursively invokes curry
once again with the target function f
and the new all arguments a
, which then get taken in as the new collected arguments array
. This repeats until the a.length
condition is met.
Side notes:
- During the first call, the all arguments
a
and the current argumentsargs
are the same value, as the collected argumentsarray
has nothing added to it yet. - The grand-child function's immediate invocation happens before its parent function completes.
Partial Application
As you learned earlier, an application happens when the arguments are used to replace the function's parameters. This allows the function to perform its task using the provided arguments.
A partial application is the process of applying a function to some, but not all, of its arguments. This creates a new function, which you can store in a variable for later use. The new function needs fewer arguments to execute because it only takes the remaining parameters as arguments.
Partial applications are useful for specialization when you want to reuse a function with common parameters.
Point-Free Style
inc
is defined in point-free style, which is when you write a function without mentioning the parameters. inc
uses closure because the argument 1
is captured in the closure of add
as a
.
There are two requirements for your code to be composable: "data last" and the data needs to line up.
Data First vs. Data Last
"data last" means the data your functions operate on should be their last parameter.
For the add
and multiply
functions you saw earlier the order of arguments is irrelevant because they are commutative.
But division is NOT, so you're going to learn the importance of the "data last" principle using a divide
function.
Let's say you want to specialize and create a halve
function that takes in a number and divides it by two.
Using the "data first" function, it's impossible for you to define a halve
function in point-free style.
halfFail
captures 2 in the closure of divideDataFirst
. It is a function that takes in a number and when called divides 2 by that supplied number.
In general, you need to write your functions using the "data last" principle to enable partial application.
BTW, the best library for functional programming in the "data last" paradigm is Ramda. Here is an outlook what you can do with Ramda. You can understand this in-depth some time in the future.
size
is a function that can take in objects or arrays and returns how many properties or elements it has.
Remember, functional programming is meant to improve your code by making it more bug-free, modular, testable, refactorable, understandable and deterministic. So, if you mix imperative and declaritive code, like in the definition for renameKeys
that is totally fine. Relax if you need some time to understand implementations like this. As you play around with this new paradigm, go with what's easiest for you to write. There is no need to force functional programming.
You can call renameKeys
with keyReplacements
and an input
to return a new object with it's keys renamed according to the keyReplacements
.
Data Needs To Line Up
Similarly, only with "data last" can you compose functions effectively because the types of the arguments and return values of functions have to line up to compose them. For example, you can't compose a function that accepts an object and returns a string with a function that receives an array and returns a number.
Mozart
Now it's your turn. Try the following exercise to practice what you've learned from this article. If you get stuck, you can always scroll up and read it again. If you prefer not to do the exercise, just read the solution and follow along.
Create a function that takes in an array of numbers and filters all the even numbers (so it rejects the odd numbers), then doubles all the even numbers and lastly, sums up the result. Break down your functions to their most basic abstractions then compose them point-free. (Hint: You'll need to look up the modulo operator %
to check if a number is even.)
Lastly, is this more complicated than simply writing the following?
Yes, absolutely! You should always use the simplest implementation for your requirements, also known as KISS (Keep It Simple, Stupid), or YAGNI (You Ain't Going To Need It).
Functional programming shines as your app grows and you need to generalize and specialize your code so it scales well and stays maintainable. This way your code is more modular, and way easier to test, to reuse and to refactor. Future articles will show you real-world examples how to use these techniques.
You now know the 20% of functional programming that gives you 80% of the result.