The two pillars of functional programming
Functional programming is based on the following two pillars:
- Referential transparency
- Composition (as universal design pattern)
All of the remaining content derives directly or indirectly from those two points.
Referential transparency
Definition. An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program's behavior
Example (referential transparency implies the use of pure functions)
const double = (n: number): number => n * 2
const x = double(2)
const y = double(2)
The expression double(2)
has the referential transparency property because it is replaceable with its value, the number 4.
Thus I can proceed with the following refactor
const x = 4
const y = x
Not every expression is referentially transparent, let's see an example.
Example (referential transparency implies not throwing exceptions)
const inverse = (n: number): number => {
if (n === 0) throw new Error('cannot divide by zero')
return 1 / n
}
const x = inverse(0) + 1
I can't replace inverse(0)
with its value, thus it is not referentially transparent.
Example (referential transparency requires the use of immutable data structures)
const xs = [1, 2, 3]
const append = (xs: Array<number>): void => {
xs.push(4)
}
append(xs)
const ys = xs
On the last line I cannot replace xs
with its initial value [1, 2, 3]
since it has been changed by calling append
.
Why is referential transparency so important? Because it allows us to:
- reason about code locally, there is no need to know external context in order to understand a fragment of code
- refactor without changing our system's behaviour
Quiz. Suppose we have the following program:
// In TypeScript `declare` allows to introduce a definition without requiring an implementation
declare const question: (message: string) => Promise<string>
const x = await question('What is your name?')
const y = await question('What is your name?')
Can I refactor in this way? Does the program's behavior change or is it going to change?
const x = await question('What is your name?')
const y = x
The answer is: there's no way to know without reading question
's implementation.
As you can see, refactoring a program including non-referentially transparent expressions might be challenging. In functional programming, where every expression is referentially transparent, the cognitive load required to make changes is severely reduced.
Composition
Functional programming's fundamental pattern is composition: we compose small units of code accomplishing very specific tasks into larger and complex units.
An example of a "from the smallest to the largest" composition pattern we can think of:
- composing two or more primitive values (numbers or strings)
- composing two or more functions
- composing entire programs
In the very last example we can speak of modular programming:
By modular programming I mean the process of building large programs by gluing together smaller programs - Simon Peyton Jones
This programming style is achievable through the use of combinators.
The term combinator refers to the combinator pattern:
A style of organizing libraries centered around the idea of combining things. Usually there is some type
T
, some "primitive" values of typeT
, and some "combinators" which can combine values of typeT
in various ways to build up more complex values of typeT
The general concept of a combinator is rather vague and it can appear in different forms, but the simplest one is this:
combinator: Thing -> Thing
Example. The function double
combines two numbers.
The goal of a combinator is to create new Things from Things already defined.
Since the output of a combinator, the new Thing, can be passed around as input to other programs and combinators, we obtain a combinatorial explosion of opportunities, which makes this pattern extremely powerful.
Example
import { pipe } from 'fp-ts/function'
const double = (n: number): number => n * 2
console.log(pipe(2, double, double, double)) // => 16
Thus the usual design you can find in a functional module is:
- a model for some type
T
- a small set of "primitives" of type
T
- a set of combinators to combine the primitives in larger structures
Let's try to implement such a module:
Demo
As you can see from the previous demo, with merely 3 primitives and two combinators we've been able to express a pretty complex policy.
Think at how just adding a single new primitive, or a single combinator to those already defined, adds expressive possibilities exponentially.
Of the two combinators in 01_retry.ts
a special mention goes to concat
since it refers to a very powerful functional programming abstraction: semigroups.