The interesting properties of pure functions

What is a pure function, why is it interesting and how do we use them at Indy?

What is a pure function?

A pure function has two properties :

  1. Output depends only on input
  2. No side effects

Output depends only on input

Output only depends on input, no matter when or where you call the function in your code.

To implement such a function, you could simply map input to output.

const square = {
	0: 0,
	1: 1,
	2: 4,
	3: 9,
	4: 16,
	…
};

square[2]; // is always 4

Writing all possible pairs (input, output) can be tedious. So you calculate the output from the input

const square = x => x * x;
square(2); // is always 4

These two implementations of square are equivalent and you always have

square(2) = square(2)

On the contrary, Math.random() is impure as the output depends on when it is called.

Math.random() ≠ Math.random()

No side effects

A side effect is an observable effect other than returning a value:

  • I/O operation
    • Writing to database or file
    • making an HTTP call
    • Logging
    • Printing on screen
  • Mutation

Being pure means leaving I/O operations outside of the function and using immutability.

const a = square(2) is equivalent to const a = 4

Nothing happens during the computation of square(2) other than returning 4.

An implementation with side effect could be

const impureSquare = x => {
	console.log('square', x);
	return x * x;
};

const a = impureSquare(2) is not equivalent to const a = 4

Why is it interesting?

As output only depends on input without side effects, pure functions are independent of time and state. Pure functions are predictable.

Referentially transparent

An expression is referentially transparent when it may be replaced by its value without changing the result of the program. It makes the code easier to read, understand and test.

const add2 = x => x + 2;
const multiplyBy2 = x => x * 2;
const add2AndMultiplyBy2 = x => multiplyBy2(add2(x));

const result = add2AndMultiplyBy2(3);
// is equivalent to
const result = multiplyBy2(5);
// is equivalent to
const result = 10;

Testable

Pure functions are easily testable.

const tomorrow = today => today.add({ days: 1 });

// You simply test it with
expect(tomorrow(Temporal.PlainDate.from(‘2022-05-28’)))
	.toEqual(Temporal.PlainDate.from(‘2022-05-29’));

What if it were an impure function?

const impureTomorrow = () => {
	const today = Temporal.Now.plainDateISO();
	return today.add({ days: 1 });
};

Impure function is tricky to test as you have to mock Temporal.Now.plainDateISO().

Cacheable

Pure function output is cacheable. Simply map input to output and the next time you need the result of this extensive function call, use the cached value. No need to worry about time to live.

To do that, you can use memoization. Here is a basic implementation:

const memoize = (f) => {
	const cache = {};
	
	return (...args) => {
		const key = JSON.stringify(args);
		
		if (key in cache) {
			return cache[key];
		}
		
		const result = f(...args);
		cache[key] = result;
		return result;
	};
};

const memoizedSquare = memoize(square);

memoizedSquare(2); // is 4. It calls square()
memoizedSquare(2); // is 4. It uses the cached result. Doesn’t call square()

Runnable in parallel

Pure functions can run in parallel as no access to shared memory means no race condition.

How do we use them at Indy?

Due to the power of pure functions, we use them at Indy.

We have two kind of functions:

  • model functions (pure) contain all the logic of the application
  • service functions (impure) handle I/O operations

Model functions

Model functions are pure and contain all the logic of the application. No logic should be in impure functions.

  • Calculating business (mainly bookkeeping) rules
  • Mapping data at interfaces (business model / database, business model / web)

Service functions

Service functions gather data, use model functions and save results. No logic here.

async function doSomething() {
	const data = await getData();
	// all the logic is in this pure function from the model
	const result = computeSomething(data);
	await saveResult(result);
}

Laisser un commentaire