DEV Community

Cover image for Real world example of compose function and currying.
Pegah Safaie
Pegah Safaie

Posted on

Real world example of compose function and currying.

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 should we use compose functions in our code?

we want to model the following ice cream production line by using javascript functions.
Alt Text

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;
}
Enter fullscreen mode Exit fullscreen mode

For a berry ice cream with chocolate topping, you might write:

 decorate(form(mix(ice, πŸ“, πŸ’, πŸ‡)), 🍫)
 // output: " scooped πŸ“, πŸ’, πŸ‡ ice cream decorated with 🍫"
Enter fullscreen mode Exit fullscreen mode

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.
Alt Text

const compose = (ice, tastes, decorateTaste) => 
    form(decorate(mix(ice, tastes), decorateTaste));

// call compose
compose('ice',['πŸ“', 'πŸ’', 'πŸ‡'], '🍫');

// output: " scooped πŸ“, πŸ’, πŸ‡ ice cream decorated with 🍫"
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text
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');
Enter fullscreen mode Exit fullscreen mode

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.
Alt Text
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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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.
Alt Text
And here is how reduceRigth works when we call composeGenerator with our piepeline functions.
Alt Text

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
joyshaheb profile image
Joy Shaheb

good job !