Composes two functions.
Composes two functions.
const compose = require('folktale/core/lambda/compose');
const inc = (x) => x + 1;
const double = (x) => x * 2;
const incDouble = compose(double, inc);
incDouble(3);
// ==> 8
Composition is a way of creating new functionality by combining existing functionality. It helps managing complexity in software projects, since newer functionality can be defined in terms of existing ones, reducing both the amount of code needed to implement newer features, and the amount of time one needs to spend to understand them.
This technique is used very often in regular JavaScript code, when we feed the results of a function into another function:
const names = [
'Alissa', 'Max', 'Talib'
];
const upcase = (name) => name.toUpperCase();
names.map(upcase).join(', ');
// ==> 'ALISSA, MAX, TALIB'
Here names
is fed into the map
function, which returns an array
of the same names in capitals. This new array is then fed into the
join
function, which returns a String where the items are separated
by a comma.
If we need to use .map(upcase).join(', ')
in a lot of places, it
makes sense to create a new function that captures that composition,
so we don't have to type it everywhere:
const showNames = (names) =>
names.map(upcase).join(', ');
Now we can use showNames
instead of .map(upcase).join(', ')
, and
we get the same functionality:
showNames(names);
// ==> 'ALISSA, MAX, TALIB'
JavaScript is a hybrid language, where you can express programs by using object oriented techniques, functional programming techniques, and some other paradigms. It's not uncommon to see parts of a program expressed in OOP, as in the examples from the previous section.
However, objects in JavaScript can't be safely extended. Ideally, we'd like to write:
names.show();
Instead of:
showNames(names);
Which would be more consistent with the rest of the program. Because this involves mutating the array, all sorts of problems could happen so it's better to keep the new functionality as a separate function.
Expressing things in functional programming doesn't have this problem because functions exist on their own, rather than being part of an object.
In functional programming, functions are the basic building block, and we compose functions to create bigger things. To do this, we often have to rephrase the common method calls in terms of regular functions:
const names = [
'Alissa', 'Max', 'Talib'
];
const upcase = (name) => name.toUpperCase();
const map = (transform, items) => items.map(transform);
const join = (separator, items) => items.join(separator);
With this we can express the composition in terms of regular function calls:
const showNames = (names) =>
join(', ', map(upcase, names));
showNames(names);
// ==> 'ALISSA, MAX, TALIB'
Now, while the way this program expresses its functions is consistent, we still have to move the whole expression into the function, and our code is not as easy to follow as before. Rather than reading from left to right, we have to first read the inside of a function application before we read what's outside of it.
These issues are not a problem with simple expressions such as these,
and most of the expressions in functional programming tend to be very
simple. But we can improve this by abstracting over the idea of
composition. That is, instead of requiring syntactic composition, by
having people write out the whole expression, we can ask the program
to combine these functions for us. This is what the compose
function
does:
const compose = require('folktale/core/lambda/compose');
const showNames2 = compose(
capitals => join(', ', capitals),
names => map(upcase, names)
);
Instead of reading from inside of a complex expression to the outside, now you can read each expression on its own, and visualise the flow of data linearly.
CONVENIENCE
compose
does right-to-left composition, so the last function is applied first. You can use theinfix
version of compose to avoid this.
Composing unary functions is trivial, as seen on the first example of this documentation:
const compose = require('folktale/core/lambda/compose');
const inc = (x) => x + 1;
const double = (x) => x * 2;
const incDouble = compose(double, inc);
But composing functions that take more than one argument doesn't read as naturally:
const upcase = (name) => name.toUpperCase();
const map = (transform, items) => items.map(transform);
const join = (separator, items) => items.join(separator);
const showNames = compose(
capitals => join(', ', capitals),
names => map(upcase, names)
);
The problem is that compose
can only safely compose unary functions,
so you have to do some additional work to place the values in the
"right places" when your function takes more than one argument.
One way to solve this problem is to curry the functions that will be composed. That is, turn a function of arity N, into N functions of arity 1:
const upcase2 = (name) => name.toUpperCase();
const map2 = (transform) => (items) => items.map(transform);
const join2 = (separator) => (items) => items.join(separator);
const showNames2 = compose(join2(', '), map2(upcase2));
Note that with a small change to how map
and join
are defined, we
can make a much better use of the compose
function. This is not
without its drawbacks, however, as now join
has to be called as
join(',')(names)
.
CONVENIECE
Folktale offers acurry
function as a convenience for automatically creating curried versions of existing functions, which also works around the 1-argument limitation.See
folktale/core/lambda/curry
for details.
Another way to work around this problem is by using the partialize
function provided by Folktale. This function creates a new function
that specifies only parts of the argument for the original function:
const partialize = require('folktale/core/lambda/partialize');
const _ = partialize.hole;
const upcase3 = (name) => name.toUpperCase();
const map3 = partialize(2, (transform, items) => items.map(transform));
const join3 = partialize(2, (separator, items) => items.join(separator));
const showNames3 = compose(
join3(', ', _),
map3(upcase, _)
);
See folktale/core/lambda/partialize
for more information on how the
partialize
function works.
The compose
operation is limited to composing only two functions.
This might sound limiting, but it ensures that function composition
is well-defined.
Because a lot of functions in JavaScript are expected to be variadic,
and just ignore the additional parameters, you'd get some very
surprising behaviour when trying to pass compose
to them. For example,
the following would throw a type error:
const compose = require('folktale/core/lambda/compose');
const inc = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;
[inc, double, square].reduce(compose)(3);
// ==> throws TypeError: '1 is not a function'
Because it's equivalent to:
const fns = [inc, double, square];
compose(
compose(
square,
double,
1,
fns
),
inc
0,
fns
)(3);
If you need to compose more than two functions, you can use the
infix
syntax, or the all
convenience function.
With the This-Binding syntax proposed for JavaScript, it's possible to compose multiple functions in an, arguably, more natural way:
const compose = require('folktale/core/lambda/compose');
const then = compose.infix;
const inc = (x) => x + 1;
const plus4 = inc::then(inc)::then(inc)::then(inc);
plus4(2);
// ==> 6
all
FunctionIf you need to compose more than two functions, you might consider using
the all
convenience function instead, which is variadic:
const compose = require('folktale/core/lambda/compose');
const inc = (x) => x + 1;
const plus4 = compose.all(inc, inc, inc, inc);
plus4(2);
// ==> 6
Composition with the all
convenience still happens from right to left.
Conveniently composes multiple functions.
Conveniently composes function with the This-Binding syntax.
(f, g) => (value) => f(g(value))