Functional programming (FP) has seen a rise in usage within the past half decade with the introduction of libraries like React. We get to use some of these libraries that encourage declarative programming and functional paradigm, yet many are intimidated by the term functional programming because it is often followed by jargon terminology like monads, functors, lambda calculus, currying, transducers...
When the year started, we explored the state of functional programming with one of X-Team's functional programming heroes Michal Kawalec, and there were mentions of languages that encourage this paradigm now adopted by many. Haskell, Clojure, Elm, Clojure, Idris, Scala, and Ocaml were briefly discussed. Each of those functional languages has its strengths and weaknesses; then there's JavaScript that was not designed in any way for functional programming.
Unlike languages that are built for functional programming, anyone using JavaScript might not have originally signed up for functional programming, so it makes sense that people hold back from getting into a concept that seems so advanced. The good part is you do not have to flip a switch into the world of FP. You can take some of its good parts and apply them to how you think about code. Then gradually apply some of this thinking to your regular code.
There are two common terms in discussions about functional programming — Imperative and Declarative programming — and it is best to have a proper understanding of them early on.
Imperative programming is an approach where we program computers by telling them how to approach a problem. It is procedural and exhaustive.
While imperative goes into the detail of how, declarative programming simply tells the computer what to do and it carries out that duty in the best way it can.
In a recent group chat, someone used this to describe these concepts non-technically
Here is a programmable distinction between them from a piece of three.js code:
// Imperative
for(let i = 0; i < geometry.vertices.length; i++){
geometry.vertices[i].x += Math.random() * 10;
geometry.vertices[i].y += Math.random() * 15;
}
// Declarative
geometry.vertices.map(vertex => {
vertex.x += Math.random() * 10;
vertex.y += Math.random() * 15
})
The first example above mutates the original geometry object which will affect the vertex values wherever we have to use it. This impact on an object outside of the loop's iteration scope is called a side effect.
The declarative example just clones the existing geometry object and performs the modifications on that clone.
Here is another example with a function to strip duplicates in an array.
// Imperative
const stripDuplicates = arr => {
let temp = [];
arr.sort(); // side-effect, mutation
for(let i = 0; i < arr.length; i++){
if(arr[i] === arr[i+1]) { continue }
temp[temp.length] = arr[i];
}
return temp;
}
// Declarative
const stripDuplicates = arr => {
return arr.filter((item, pos) => {
return arr.indexOf(item) === pos;
});
}
This example shows a great difference between both by abstracting much of the human work and preventing the side effects caused by the imperative alternative.
Abstractions make the code much more readable. Humans comprehend declarative code and computers understand imperative code better. However, we have compilers to translate our declarative code to imperative code for computers, so we do not have to task ourselves with that.
Declarative programming, when done properly, will lead to less mutation in the code. An immutable code is one with fewer side effects, which helps with garbage collection, among many other things, and code maintenance. There are native functional methods in JavaScript that we could start leveraging to reduce mutation in our code, but how do we write functions that have no side effects?
A function without side effects is considered a pure function. To prevent side effects, a function must not perform I/O operations or modify the state of data outside its scope. This can get tricky with some JavaScript datatypes. As explained in this detailed article, there is a distinction between values and references and they affect how we use datatypes. Primitive types are passed by value. They include Boolean, String, Number, undefined, null, and they are computationally cheap to use.
Technically, there is no such thing as object literals. Objects, Arrays, and Functions get stored in memory when defined, and a referential address referencing them is stored in the variable. So what we have when these datatypes are defined is really a referential address literal, but I doubt if that is an actual term used by anyone.
Based on the example from the linked article,
const changeAgeImpure = person => {
person.age = 25;
return person;
};
const changeAgePure = person => {
const changedPerson = JSON.parse(JSON.Stringify(person));
changedPerson.age = 25;
return changedPerson;
};
const alex = {
name: 'Alex',
age: 30
};
const changedAlex = changeAgePure(alex);
when a function takes in any datatype in Object form, it will act upon the address which is also referenced outside of the function scope.
The changeAgePure
function kills the reference to the initial person object address and creates a clone for use within the function scope only. This level of purity also makes it easier to write unit test for functions and ascertain they would not be affected by foreign code. By writing code this way, we somewhat abide by the law of demeter that says
Each unit should only talk to its friends, don't talk to strangers
We can be aware of the existence of strangers, but we should not speak to them in our function scope. It also increases cohesion and reduces coupling.
The necessity for predictable function arguments is a good reason why static typing plays a big role in functional programming and is used in most functional languages.
This is a term that is so common among functional programmers, and I had to look up the stackoverflow definition many times but it was never clear until I actually got into functional programming. I will still recommend it for anyone that needs a proper definition. To me, a function is considered to have referential transparency if it can output the same result as an equivalent procedure when given a specific input. Much simpler, it is a function that will always give the same output when called with the same inputs. This makes every pure function referentially transparent, but not every referentially transparent function is a pure function.
Arguments are an important part of functions. It is therefore important to understand how they may affect our functions. The number of arguments in a function is known as arity. Functions with single arity are unary functions, 2 arity makes binary functions, 3 arity to ternary functions, and more than 3 arity will give a n-ary function.
Sometimes we write functions that really need to take more parameters but we do not want ternary or n-ary functions. We often employ objects as parameters or pass in a function as argument.
A function that can accept another function (a callback) as a parameter is a higher-order function. Passing a function to a higher-order function is made possible because JavaScript functions are first class citizens. This means we can assign functions as values to variables, pass them to other functions, and return them. This is only possible in some programming languages.
function foo() {
// Function declaration
}
const foo = () => {
// Function expression : a use of function as
// first-class citizen
// An anonymous function literal
}
Examples of higher order functions include the map, filter, and reduce methods of the Array prototype.
One might wonder how exactly Functional Programming differs from a paradigm like Object Oriented Programming (OOP). The major distinction is that OOP as a paradigm encourages two main things to improve our programs — encapsulation and separation of concerns.
The same goals can be achieved with FP, and some of its SOLID principles, like the single responsibility principle, can be applied to FP. Where functional programming shines is in how it helps developers in thinking about how to avoid side effects. Even though it is hardly possible to have a program that is completely free of side-effects, because most programs need to interact with network or IO at some point, we can separate code with side effects and avoid it in the majority of our functions. When you build a program and find out it drains your users' batteries or consumes a lot of memory, you find the problem faster in a FP than in an OOP codebase.
The goal of this post is not to sway you from other good paradigms like OOP. It is totally fine to still use them or have them mixed up with some FP. Now is the time to get that FP on!
Continue Reading: Functional Programming: Composition and Associativity