avatarHéla Ben Khalfallah

Summary

Functional programming simplifies software development by using a pipeline of pure functions, emphasizing declarative code, immutability, and easy-to-test functions.

Abstract

Functional programming is characterized as a sequence of pure functions that transform inputs without mutation, akin to a pipeline in a warehouse. This approach is declarative, focusing on what needs to be done rather than how, and it enables code to be more predictable, easier to debug, and simpler to unit-test. The article explains that functional programming relies on composing small, single-responsibility functions to build complex operations, and it introduces higher-order functions and chaining as key techniques. Safety in functional programming is addressed through the use of functors and monads, such as the Maybe monad, which handle errors gracefully and ensure that functions can be chained without causing unintended side effects.

Opinions

  • The author views functional programming as a beneficial paradigm for creating reliable and maintainable code.
  • Higher-order functions are praised for their ability to abstract and reuse code, reducing repetition and increasing clarity.
  • The article emphasizes the importance of immutability and pure functions in functional programming to achieve predictability and determinism.
  • Functors and monads, particularly the Maybe monad, are presented as essential tools for safe function chaining and error handling in functional programming.
  • The author suggests that functional programming's composability and isolation of functions lead to better unit testing and overall code quality.
  • The use of real-life warehouse operations as an analogy for functional programming concepts is intended to make these concepts more accessible and understandable to the reader.

Functional Programming, Simplified

A visual introduction to functional programming

Photo by Jackson Jost on Unsplash.

What’s Functional Programming?

Functional programming is a pipeline of pure functions.

What does that mean?

To understand this definition, we will use a real-life example. Let’s assume that we have a shipping warehouse and a large store containing a variety of products (books, clothes, certain canned food products, household appliances, etc.).

How do things work inside our warehouse?

Inside the warehouse — Image by the author

Our workflow is a chain of small processing steps arranged so that the output of each element is the input of the next one: a pipeline. Each step exists only once.

The unit of work of a functional code is the function. Each function is unique (single responsibility and DRY — Don’t Repeat Yourself).

Functional programming uses pipelines as its unique method of building.

Functions pipeline — Image by the author

In math, a function is a short formula that connects outputs to inputs. The same input always gives the same output. In a short formula, variations and behaviors are resumed in a single short relation.

Functional programming is declarative (what is more important than how).

Let’s see these two ways to double the items of an array:

  • double1 is an imperative code that details, step by step, how to double each element of the array.
  • double2 is a declarative and concise code that reduces the structure of a function down to the most important pieces (a formula).

Did the products change during the warehouse workflow?

No! Going from one stage to another, we add new accessories without mutating or changing the original product.

For example, in the Assorting step, we group ordered products inside a package and then proceed to the Weighing step. When the final user receives the order and unwraps the package, they will be able to get products without mutations or damages.

This is how functional programming works: composing functions to transform inputs without mutating them.

Immutable inputs through function transformations — Image by the author

A function never mutates its input variables but creates new, enriched outputs. A function that doesn’t mutate its inputs is a pure function.

Recap

  • The unit of work of a functional code is the function.
  • Functional programming is a pipeline of pure functions.
  • Functional programming is declarative (what > how).

What is the consequence of this?

Isolate and replace

Each stage in our warehouse is independent of the others and does not know of their existence (before or after). Similarly, each function depends only on its inputs. There is no shared state or outside dependency.

If anything goes wrong, we can easily detect the source and isolate the defective component to quickly resume.

Easy to isolate errors and replace the defective component — Image by the author

If we can easily isolate a function, we can easily unit-test it!

Each function acts as an independent and isolated microservice.

Functions are like mathematical functions:

  • A function depends only on its inputs.
  • A function doesn’t produce any side effects. For instance, f(x) doesn’t change the input x but produces a new value.

Expandable and easy to rearrange

What if we want to add a new step called Automatic Supervision between Weighing and Loading to check that everything is included inside the pack (quality improvement)?

Adding the Automatic Supervision step — Image by the author

To be effectively operational, the steps’ inputs and outputs must be compatible:

  • The outputs of Weighing should be compatible with the inputs of Automatic Supervision.
  • The outputs of Automatic Supervision should be compatible with the inputs of Loading.

That’s all! We don’t need to know how each step works internally (weak coupling)!

If we want to add a function to keep only items greater than four between the double and sort functions:

Insert new function — Image by the author

Functions inputs and outputs must be compatible in type and arity:

  • Type — The type returned by one function must match the argument type of a receiving function.
  • Arity — A receiving function must declare at least one parameter in order to handle the value returned from a preceding function call.

Connecting functions must be compatible in terms of arity and type.

We can also easily rearrange things when their type and arity are compatible.

Rearrange — Image by the author

Predictable and deterministic

In a functional programming world:

  • There is no shared state.
  • Functions are pure (immutability).
  • Each function acts as an independent and isolated microservice (free of side effects).
Functional programming filters out all external noise — Image by the author

If the instability factors are filtered out, we can easily predict the outputs at any time.

We can create a time machine that allows us to move forward or backward in the life of the program:

Predictable behavior — Image by the author

Predictability, determinism, and isolation are the code qualities needed to have good unit tests!

Reducing complexity

Functional programming is the composition of short, single-responsibility, and pure functions.

What does it mean? Let’s look at an example.

How can we calculate (2x+3)²?

  • The first step is to calculate (2x+3).
  • Then square the output.
f(x) = (2x+3)
g(x) = x²
h(x) = (g º f)(x) =  g(f(x)) = (2x+3)²

A complex mathematical function is the composition of small simple functions!

Compose to reduce complexity — Image by the author
const finalOutput 
= double º sort º display 
= display(sort(double(array)))
  • Calculate double(array).
  • Then pipe the result to sort.
  • Then pipe the result to display.

The key is composing small steps instead of having a function that does everything.

What do we gain from this? It’s easy to isolate, debug, and test!

There are many patterns of composition. The most important are higher-order functions and chaining.

Let’s dive in!

Higher-Order Functions

“Functions that operate on other functions, either by taking them as arguments or by returning them, are called higher-order functions.” — Eloquent JavaScript

What does that mean?

Higher-order function — Image by the author

transform is a higher-order function that takes f as an argument.

Why? Let’s look at how transform can be written:

transform is pure: We return a copy instead of mutating the original array.

const data = [1, 4, 2, 8];
const double = x => x * 2;
const plusOne = x => x + 1;
console.log('double array : ', transform(data, double));
console.log('plus one array : ', transform(data, plusOne));

transform contains the repeated code to iterate over an array and apply the function to its items.

It’s a way to reuse code!

Instead of repeating the same code, we create a generic higher-order function (abstraction).

Do we find transform implemented natively in a programming language or is it just a mathematical concept?

Let’s see.

Higher-order functions in JavaScript

In the JavaScript world, we have these available ways:

  1. map to transform an array:
map to transform an array — Image by the author
const sequence = [1, 2, 3, 4, 5];
const doubleSequence = sequence.map((item) => item * 2); // [2,4,6,8,10]

map:

  • Returns a new array without affecting the original array (pure and without side effects).
  • Input array length = output array length.
How JS map works — Image by the author

2. filter to filter an array:

filter an array — Image by the author

f is the predicate function.

const isString = value => typeof value === 'string';
const values = [12, 'Hi', 1, 'Sun', 'Sky', 8];
const valuesMatched = values.filter(isString);
console.log('valuesMatched : ', valuesMatched); 
// "valuesMatched : ", ["Hi", "Sun", "Sky"]

filter:

  • Returns a new array without affecting the original array (pure and without side effects).
  • The output array length depends on how many elements match the predicate (max = input array length).
How JS filter works — Image by the author

3. every and some.

every tests whether all the elements of an array satisfy a condition :

[12, 5, 8, 130, 44].every(elem => elem >= 10); // false
[12, 54, 18, 130, 44].every(elem => elem >= 10); // true
[{a:1, b:2}, {a:1, b:3}].every(elem => elem.a === 1); // true
[{a:2, b:2}, {a:1, b:3}].every(elem => elem.a === 1); // false

some tests if at least one element of the array passes a condition:

[2, 5, 8, 1, 4].some(elem => elem > 10); // false
[12, 5, 8, 1, 4].some(elem => elem > 10); // true (12)

How to Chain Functions

Chaining functions gives you the possibility of assembling small behaviors to obtain a complex result:

Chaining small functions — Image by the author
const complexArray = [6, 2, 4, 8]
.map(x => x * 2)
.map(x => x + 1)
.filter(x => x > 10);
console.log('complexArray : ', complexArray); 
// "complexArray : ", [13, 17]

Connecting functions must be compatible in terms of arity and type.

Safety in Functional Programming

Functors

Let’s look at the code below:

const increment = v => v + 1
increment('5') // '51'
increment({ v: 5 }) // "[object Object]1"

What’s wrong? {v: 5} is an object.

How can we protect our function?

Let’s now add a function that calculates the double of a number:

const double = (v) => {
  if (typeof v !== 'number') {
    return NaN
  }
  return v * 2
}

What’s wrong? There is duplicate code between increment and double.

Is there a better way? Let’s see:

Magic! Let’s understand what’s happenening:

  • For a given input, we check if it’s a number.
  • If it is, then NumberBox can apply the function (fn) and pipe the result to the next function.
  • Otherwise, the function isn’t applied and Nan is piped to the next function.
  • The context is saved between applyFunction (pipe).
  • The same check continues until the last map, where NumberBox returns the last saved value in the context.
The magic behind NumberBox — Image by the author

Instead of passing the value to the functions, we passed the functions to the value.

We can make a small change to our NumberBox:

We change applyFunction with map:

Yes, this is our map and this is how we safely chain between functions!

Then what’s a functor?

In essence, a functor is nothing more than a data structure that you can map func- tions over with the purpose of lifting values into a wrapper, modifying them, and then putting them back into a wrapper. It’s a design pattern that defines semantics for how fmap should work. — Luis Atencio, Functional Programming in JS

Perfect!

Their practical purpose is to create a context or an abstraction that allows you to securely manipulate and apply operations to values without changing any original values. This is evident in the way map transforms one array into another without altering the original array; this concept equally translates to any container type. — Luis Atencio, Functional Programming in JS

This is what we want! Safe chaining!

Are functors sufficient?

No. Why not?

When something goes wrong, functors safely continue the execution until the last map and return the last saved value in the context:

const safeResult = NumberBox({
  v: 5})
.map(v => v * 2) // -> executed
.map(v => v + 1) // -> executed
.value // NaN

Functors safely continue the execution (map) even if an error occurred.

We will solve this problem by using an advanced version of a functor called a monad.

Let’s see!

The Maybe monad

How to write a Maybe:

A magical example:

A Maybe monad is a functor because we also map the function.

However, when a Maybe encounters an issue, it doesn’t continue. It skips and goes to the default statement: getOrElse.

Functor vs. Maybe — Image by the author

Yes, it’s magic!

A Maybe has two flows: one for success and one for failure. By comparison, the functor only has one flow:

Maybe flow — Image by the author

How is this helpful?

A concrete example of use would be processing a response received from a backend. We don’t know if the data received is defined or if it respects the expected type and format.

Therefore, we can wrap the data with Maybe and chain the transformation functions. If one step fails, all steps fail.

MayBe(Backend.Call()).map().map().getOrElse();

That’s all for this article about functional programming!

Conclusion

In this article, we discussed what functional programming is, its benefits, and its available patterns.

Functional programming is the composition of short, single-responsibility, and pure functions. Pure means that function will never mutate its inputs. The function will never create or cause a side effect.

Functions are more like mathematical functions: declarative and concise (formula).

What do we gain from this?

  • We can easily isolate functions and thus easily unit-test them.
  • Since each function acts as an independent and isolated microservice, we can easily expand and rearrange the global behavior. How? By using techniques like composition, higher-order functions, and chaining.

Finally, we saw some safety tips on functional programming:

  • Functors help us to safely chain functions that are applied on a value. However, they don’t handle errors and exceptions.
  • The Maybe monad enriches functors with the capability of handling errors.

Further Information

Programming
Functional Programming
JavaScript
React
Software Engineering
Recommended from ReadMedium