Another currying article
Using Javascript, you can decide to write your code based on FP or OOP principles. When you decide on FP there are some concepts you need to understand in order to make the most out of FP principles. These include concepts like currying and compose functions. For me it took a while to understand what the currying is and when and how I should use it in my code. Here, I tried to explain what I found in a simple way, hopping to make the learning process quicker and smoother for you.
- When to use compose functions?
- How to use compose functions?
- How to enhance compose functions using currying?
- Homework
- Your opinion
When should we use compose functions in our code?
we want to model the following ice cream production line by using javascript functions.
We see a sequence of 3 actions following one another:
- Mix the ice cream with sth like ๐, ๐ and ๐.
- Decorate the ice cream with sth like ๐ซ.
- Form the ice cream scoopes.
All actions take ice cream as input, modify it with some settings(berries or chocolate) and send the modifed ice cream to the ouput to be used by next function.
Here is the atomic function for each action.
function mix(ice, tastes) {
return tastes.join(', ') + ice;
}
function decorate(ice, taste) {
return 'decorated with ' + taste;
}
function form(ice) {
return 'scooped ' + ice;
}
For a berry ice cream with chocolate topping, you might write:
decorate(form(mix(ice, ๐, ๐, ๐)), ๐ซ)
// output: " scooped ๐, ๐, ๐ ice cream decorated with ๐ซ"
I'm sure you've seen this pattern in your code:
Modifying a single data (ice cream) by a couple of operations to create the desired outcome (scooped berry ice cream with chocolate).
But this way of writing function sequences is not quite nice. The brackets are too many, and the execution order is from right to left.
To write it better, we can use the Composition Function concept in math:
Having
- f: x -> y
- g: y -> z
we can create a third function which receives a single input(x) and creates an output(z)
- h: x -> z
it looks like
- h(x) = g(f(x))
3 steps to write a better function sequence using the composition function in JS
1. Create a new compose function
For me the simplest compose function would be a wrapper function, which receives all required inputs and returns the results of the function sequence execution.
const compose = (ice, tastes, decorateTaste) =>
form(decorate(mix(ice, tastes), decorateTaste));
// call compose
compose('ice',['๐', '๐', '๐'], '๐ซ');
// output: " scooped ๐, ๐, ๐ ice cream decorated with ๐ซ"
2. Reduce the compose function's input parameters
Compose function should take only one single input. This is the data that gets modified throught the function sequence and comes out as output. In our example ice cream is this data.
It matters to keep compose function unary because when calling compose function we only want to focus on the data that is sent to the method and not care about the setting parameters.
As you see in the above picture, Each action(mix, decorate) can be customized by its corresponding setting parameters(berries and chocolate):
// Customized version of mix function using berries
const mixWithBerries = ice => mix('ice', ['๐', '๐', '๐']);
// Customized version of decorate function using chocolate
const decorateWithChoclate = ice => decorate('ice', '๐ซ');
// Compose function accepts just one single input
const compose = (ice) => form(decorateWithChoclate (mixWithBerries(ice)));
// Call compose. looks nicer!
compose('ice');
3. A more elegant generic way of creating compose functions
In this section we write a compose function generator. Why? Because it is more convenient to use a compose function generator rather than to write a compose function every time if you use compose functions a lot.
You can skip this section if you want to use composeGenerator function available in lodash/fp and ramda libraries.
We also implement our compose function generator in a more elegant fashion than our previous implementation of compose function, where we still have a lot of brackets and the execution order is still from right to left.
Then compose function generator is a function that takes a series of functions(fn1, fn2, ..., fnN) as input parameters and returns a new function(compose). The returned compose function receives data and executes functions(fn1, fn2, ..., fnN) in a given order.
That looks like this:
const composeGenerator = (fn1, fn2, fn3) => data => fn1(fn2(fn3(data)))
// create compose function using composGenerator
const compose = composeGenerator(form, decorate, mix)
compose('ice')
// or
composeGenerator(form, decorate, mix)('ice')
The double arrow in the code above indicates a function composegenerator(fn1, fn2, fn3)
which returns another function compose(data)
.
This implementation of composeGenerator is limited to 3 functions. We need something more generic to compose as many functions as you want:
const composeGenerator = (...fns) => data =>
fns.reduceRight((y, fn) => fn(y), data)
const compose = composeGenerator(form, decorateWithBerries , mixWithChoclate )
compose('ice')
// or
composeGenerator(form, decorateWithBerries , mixWithChoclate )('ice')
It's not easy but at least you define it once, and then you don't have to worry about the complexity anymore. Let's break it down into a group of smaller parts to make it easier to understand.
And here is how reduceRigth works when we call composeGenerator with our piepeline functions.
Enhance your compose function with currying
Our solution to remove the setting parameter from our compose function is not good since we will have to write new custom function every time we wish to add a new flavor to our pipeline:
// Change the production line to decorate with ๐
const decorateWithStrawberry = ice => decorate('ice', ['๐']);
composeGenerator(form, decorateWithStrawberry , mixWithChoclate )('ice');
// Change the production line to decorate with ๐ and ๐ซ
const decorateWithChocAndStrawberry = ice => decorate('ice', ['๐', '๐ซ'])
composeGenerator(form, decorateWithChocAndStrawberry , mixWithChoclate )('ice')
Our solution is to implement the curry function, which accepts the tastes and returns the decorate function with one single argument.
// Currying decorate function
const curriedDecorate = (tastes) => (ice) => decorate(ice, tastes);
// Currying mix function
const curriedMix = (taste) => (ice) => decorate(ice, taste);
composeGenerator(
form,
curriedDecorate('๐ซ') ,
curriedMix(['๐', '๐', '๐]))('ice')
Like compose functions, we may write our curried functions ourselves or create a generic function that returns a curried version of a function.
You can skip this section if you want to use curry function available in lodash/fp and ramda libraries.
A curry function receives a function fn
as input. If the passed arguments(args.length
) are at least equal to the function fn
's required arguments(fn.length
), it will execute function fn
, otherwise it will return a partially bind callback.
const curry = fn => () ({
const args = Array.prototype.slice.call(arguments)
return args.length >= fn.length ?
fn.apply(null, args) :
currify.bind(null, ...args)
})
curry(decorate)(['๐','๐ซ']) //output: a function which just needs ice cream as input
When we execute a curryFunction(curriedDecorate) with all the setting parameters(decorateTaste), it returns a new function which only needs one data parameter, and we can use it in our compose function.
A homework for you:
Generally, remember that currying is used to decrease the number of parameters of a function. In our last example, we saw that reducing inputs to a single one can be beneficial when using a compose function but unary functions can be used in more cases where we only require a single argument. For example in arrow functions we can remove the brackets when function just has one parameter:
// ๐
[1,2,3].map(function(digit) {
return digit * 2
})
// ๐
[1,2,3].map(digit => digit * 2)
As a pratice try to improve this code using currying.
const pow = (base, exponent) => Math.pow(base, exponent)
const digits = [1,2,3];
const exponent = 2;
digits.map(digit, function(digit) {
return pow(digit, exponent)
})
you can find the solution in this video from Derick Bailey
Your opinion
What is your favorite example of using currying in your code? And generally do you like using it or do you think it makes the code unnecessarily complicated ?
Top comments (1)
good job !