Thomas Rubattel
5 min readFeb 11, 2021

--

Cobblestones can be combined as functions can — Credits Markus Winkler on unsplash.com

Last updated on 2023–01–14

Function composition vs function decorator

Function composition and function decorator are two different techniques for combining functions.

In programming, the function is one of the most important abstraction for building application, especially as functional programming is gaining momentum.

It is thus quite important to know these two techniques and also their differences since they are two different concepts.

Function composition

Function composition has its roots in mathematics. This technique is used in different areas of mathematics, such as calculus, category theory, lambda calculus and combinatory logic.

Function composition plays such a fundamental role in development so that developers use it on a daily basis. Let’s see some examples.

const student = {
name: "Yoko",
grades: [3, 6, 4, 3],
};

const hasPassed = (average) => average >= 4;

const getAverage = (grades) => grades.length ?
grades.reduce((acc, grade) => grade + acc, 0) / grades.length : 0;

const isPromoted = (student) => hasPassed(getAverage(student.grades));

isPromoted(student); // true

Chaining filter with map is also a case of function composition.

const students = [
{ name: "Yoko", grades: [3, 6, 4, 3] },
{ name: "Masamune", grades: [3, 6, 4, 2] },
{ name: "Ryouko", grades: [3, 6, 4, 5] },
];

const hasPassed = (student) => student.average >= 4;

const getAverage = (grades) => grades.length ?
grades.reduce((acc, grade) => grade + acc, 0) / grades.length : 0;

const withAverage = (student) => ({
...student,
average: getAverage(student.grades)
});

const arePromoted = students
.map(withAverage)
.filter(hasPassed);

/* [
{ name: "Yoko", grades: [3, 6, 4, 3], average: 4 },
{ name: "Ryouko", grades: [3, 6, 4, 5], average: 4.5 },
]
*/
const authorPromise = fetch("/article/9")
.then( (article) => article.json() )
.then( (article) => fetch(`/author/${article.authorId}`) )
.then( (author) => author.json() );

Function decorator

Function decorator is a less known pattern than the function composition but is nevertheless a very powerful one.

A decorator is a function adding a new feature to an existing function passed as argument, called the decoratee. That latter function is not altered and thus, the decorator returns a new function.

The generic pattern of the function decorator is shown in the snippet right below. As you can see, the decoratee’s parameters and the decoratee itself are decoupled by using the currying technique. This allows the new function to have the same API as the original one, the decoratee.

const decorator = (f) => (..args) => {
// do something

f(...args);

// do something
};

Let’s discuss an example of a function decoration.

const memoize = (f) => {
const cache = {};

return (...args) => {
const argString = JSON.stringify(args);
cache[argString] = cache[argString] || f(...args);
return cache[argString];
};
};

const getAverage = (grades) => grades.length ?
grades.reduce((acc, grade) => grade + acc, 0) / grades.length : 0;

const getAverageMemoized = memoize(getAverage);

const grades = [4, 6];
const avg0 = getAverageMemoized(grades); // 5
const avg1 = getAverageMemoized(grades); // 5, hit the cache

In the snippet right above, the decorator provides a memoization feature. This way any function can be enriched by that feature. In this example getAverage is the decoratee. The new function — getAverageMemoized— prevents a computation to be redone — in this case the average of numbers.

Debouncing, throttle, Higher-Order Component in React, are other examples of function decorator, just to mention a few.

Differences

With function composition, the type of the output of the most nested function has to match with the type of the input of the next outer function and so on.

Function decorator does not have such a strong constraint.

When the decoration — means the feature to be added — and the decoratee are not composable, then you will have to go for the function decorator. For example you’d like to log the time a function takes to execute. This feature does not return anything and thus no composition can be done.

Function decorator in TypeScript

Let’s rewrite the previous example in TypeScript. We want the decorator to be a generic function in order to have any function to be possibly decorated. While calling the decorator, the type of the decoratee is passed around, this way decorator<decorateeType>(decoratee).

TypeScript 3.1 introduced a new utility type called Parameters<T>. Parameters<T> returns the type of a function’s arguments in form of a tuple. Now, we can use that type generic in the decorator based on passed decorateeType.

const memoize = <F extends (...args: any) => any>
(f: (...args: Parameters<F>) => ReturnType<F>) => {

const cache : Record<string, ReturnType<F>>= {};
return (...args: Parameters<F>) => {
const argString = JSON.stringify(args);
cache[argString] = cache[argString] || f(...args);
return cache[argString];
};
};

const getAverage = (grades: number[]) => grades.length ?
grades.reduce((acc, grade) => grade + acc, 0) / grades.length : 0;

const getAverageMemoized = memoize<typeof getAverage>(getAverage);

const grades = [4, 6];
const avg0 = getAverageMemoized(grades); // 5
const avg1 = getAverageMemoized(grades); // 5, hit the cache

Recursive decorator

As we discussed above a decorator is a function enriching another one passed as argument.

Now, let’s assume that a decorator takes a decorator as argument. This means, in this case, that the decoratee is a decorator itself. This is a case of recursion.

Let’s illustrate this idea in the simplest way.

const chain = (...funcs) => {
if (funcs.length === 0) {
return (x) => x;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

const log1 = (next) => (a) => {
console.log("log1 before");
next({...a, log: 1});
console.log("log1 after");
};

const log2 = (next) => (a) => {
console.log("log2 before");
next({...a, log: 2});
console.log("log2 after");
};

const log3 = (next) => (a) => {
console.log("log3 before");
next({...a, log: 3});
console.log("log3 after");
};

chain(log1,log2,log3)(x => x)({ type: "todo/add", payload: "Buy food" });

/*
log1 before
log2 before
log3 before
log3 after
log2 after
log1 after
*/

In the snippet right above, chain automates the creation of the final decorator, so that any number of decorators can be passed around. For the sake of the example, the decoratee is x => x . Thus, log3 decorates x => x. The resulting new function is decorated by log2. In turn, the resulting new function is decorated by log1.

Onion model — recursive decoration

This is the way Redux implements the support of middlewares. A middleware is a layer providing a service. A layer is a function decorating the next layer and so on recursively. In Redux, the decoratee is the reducer — to be precise, a wrapper around the reducer.

The argument of each layer is the action in the case of Redux. One of the roles of a middleware is to modify the action. In other framework supporting middleware, like Express.js, a middleware extends the request object. As you can see in the example, the action is first passed with {type: "todo/add", payload: "Buy food"} and is changed in each layer.

A layer can also interrupt the handover to the next layer by not calling next under certain circumstances. This way the decoratee will never be finally called. To make an analogy with Redux, this means that the reducer will not be called, therefore the state will not be changed.

--

--