DEV Community

loading...

Compose your function for a better reusability

aminnairi profile image Amin ・6 min read

Today we will learn to implement a curry function in JavaScript. This function will help us create more re-usable functions.

What is currying?

Let's say we are in a real context. Your mission is to write a function that will say hello in a special way. The first parameter will be a prefix that can be added before the message. The second will be the last name of the person to greet and the third will be the first name.

function greet(prefix, lastname, firstname) {
  return `${prefix} ${lastname}, ${firstname}`
}

console.log(greet("Hello", "ALLEN", "Barry"))
console.log(greet("Hello", "ALLEN", "Nora"))
console.log(greet("Hello", "ALLEN", "Henry"))
console.log(greet("Hello", "ALLEN", "Bart"))

// Hello ALLEN, Barry
// Hello ALLEN, Nora
// Hello ALLEN, Henry
// Hello ALLEN, Bart

But we are not satisfied because there is a lot of repetition. Especially for the prefix and the last name that seem to repeat a lot in our code. Let's fix this.

function greet(prefix, lastname, firstname) {
  return `${prefix} ${lastname}, ${firstname}`
}

const prefix = "Hello"
const lastname = "ALLEN"

console.log(greet(prefix, lastname, "Barry"))
console.log(greet(prefix, lastname, "Nora"))
console.log(greet(prefix, lastname, "Henry"))
console.log(greet(prefix, lastname, "Bart"))

// Hello ALLEN, Barry
// Hello ALLEN, Nora
// Hello ALLEN, Henry
// Hello ALLEN, Bart

That's better! But you feel like you are repeating yourself a lot. What could we do to prevent repeating the prefix and lastname in the calls for the greet function? We could write it that way.

function greet(prefix, lastname) {
  return function(firstname) {
    return `${prefix} ${lastname}, ${firstname}`
  }
}

const prefix = "Hello"
const lastname = "ALLEN"
const greetAllens = greet(prefix, lastname)

console.log(greetAllens("Barry"))
console.log(greetAllens("Nora"))
console.log(greetAllens("Henry"))
console.log(greetAllens("Bart"))

// Hello ALLEN, Barry
// Hello ALLEN, Nora
// Hello ALLEN, Henry
// Hello ALLEN, Bart

Great! That's more of a re-usable function we wrote there. What we did was to defer the final return by using a function as a return value. This is called a closure. It will remember its previous context (the prefix & lastname variables) in order to use them in the next call which will effectively return the formated string. But then we want to greet more people.

function greet(prefix, lastname) {
  return function(firstname) {
    return `${prefix} ${lastname}, ${firstname}`
  }
}

const prefix = "Hello"
const lastname = "ALLEN"
const greetAllens = greet(prefix, lastname)

console.log(greetAllens("Barry"))
console.log(greetAllens("Nora"))
console.log(greetAllens("Henry"))
console.log(greetAllens("Bart"))
console.log(greet("Hello", "QUEEN")("Oliver"))

// Hello ALLEN, Barry
// Hello ALLEN, Nora
// Hello ALLEN, Henry
// Hello ALLEN, Bart
// Hello QUEEN, Oliver

We had to call our function twice to greet Oliver QUEEN. It works, but it feels unnatural. What if we want another prefix message? We would have to update our function accordingly.

function greet(prefix) {
  return function(lastname) {
    return function(firstname) {
      return `${prefix} ${lastname}, ${firstname}`
    }
  }
}

const greetAllens = greet("Hello")("ALLEN")
const greetQueens = greet("Welcome")("QUEEN")

console.log(greetAllens("Barry"))
console.log(greetAllens("Nora"))
console.log(greetAllens("Henry"))
console.log(greetAllens("Bart"))
console.log(greetQueens("Oliver"))
console.log(greetQueens("Robert"))
console.log(greetQueens("Moira"))

// Hello ALLEN, Barry
// Hello ALLEN, Nora
// Hello ALLEN, Henry
// Hello ALLEN, Bart
// Welcome QUEEN, Oliver
// Welcome QUEEN, Robert
// Welcome QUEEN, Moira

But then again, something's off. It does not feel natural at all. What if we want to greet a single person?

greet("HI")("DOE")("Jhon")

It is clear now that we need to find a solution that would work for both cases: either I have a function that I want to partially call or call it with the right amount of parameters. This is called currying.

How to implement currying in JavaScript?

What I am going to show you is my way of defining the curry function in JavaScript. I'm pretty sure there other variants available for this purpose but what is important is to get the idea behind this definition.

Here is how we will end up using our function:

const greet = curry(function(prefix, lastname, firstname) {
      return `${prefix} ${lastname}, ${firstname}`
})

const greetAllens = greet("Hello", "ALLEN")
const greetQueens = greet("Welcome", "QUEEN")

console.log(greetAllens("Barry"))
console.log(greetQueens("Oliver"))
console.log(greet("Hi", "DOE", "Jhon"))

// Hello ALLEN, Barry
// Welcome QUEEN, Oliver
// Hi DOE, Jhon

And there we go. We now have a function that can be called like a regular function or be curried. Let's see how to implement this in JavaScript now.

The implementation

As we saw in the example above, the curry function will wrap our function definition. Obviously, this is a hint for us because it will certainly take a function (callback) as its parameter and return a function (a closure). We'll see the inner logic of the closure later. Let's begin with what we know.

function curry(callback) {
  return function() {
    // ...
  }
}
                   callback
                      |
                      |
                      v
const greet = curry(function(prefix, lastname, firstname) {
      return `${prefix} ${lastname}, ${firstname}`
})

That's great, but that is not enough. First. Let's cover the regular call to this curried function. We need to find a way to gather all the parameters that will be passed to the function, but we do not know how many parameters will be passed. So we will use the destructuring operator to gather the parameters in an array. We will use the spread operator and call our callback with all its parameters when the closure is called.

function curry(callback) {
  return function(...parameters) {
    return callback(...parameters)
  }
}
         parameters
            /\
           /  \
          /    \
         /      \
        /        \
        |         |
        |         |
        V         V
greet("Hello", "ALLEN")

And this will already work for this case:

function curry(callback) {
  return function(...parameters) {
    return callback(...parameters)
  }
}

const greet = curry(function(prefix, lastname, firstname) {
      return `${prefix} ${lastname}, ${firstname}`
})

console.log(greet("Hi", "DOE", "Jhon"))

But it wont work when we try to make re-usable partial application of our greet function. We need to add some more code to make it work in both case.

What I'll use is recursion. The idea is that as long as the number of parameters passed in our closure is not enough, I'll return another curried function and keep gathering parameters in an array. When we finally have the necessary number of parameters, we will be able to call our callback with all its needed parameters.

Let's first try to check when to return the callback and when to return a curried function.

function curry(callback) {
  return function(...parameters) {
    if (parameters.length >= callback.length) {
      return callback(...parameters)
    }
  }
}

Here I say that if the parameters passed are enough to satisfy the function signature, we call the function with all its parameters. That does not change what has been done until now. Obviously, the interesting part is what is happening when we do not have the necessary parameters.

function curry(callback) {
  return function(...parameters) {
    if (parameters.length >= callback.length) {
      return callback(...parameters)
    }

    return curry(callback)
  }
}

And now we have the core idea. We simply call our curry function recursively on our callback until there is enough parameters. But if you look closely, what we did here is not correct. When we will try to provide less parameters than required, the function will simply call the curry function on itself. But then, we haven't stored our parameters in the next call to curry. We will never be able to gather the parameters that way. We need to pass a second argument in our recursive call.

function curry(callback, ...oldParameters) {
  return function(...parameters) {
    if (parameters.length >= callback.length) {
      return callback(...parameters)
    }

    return curry(callback, ...parameters)
  }
}

That's great, we are almost there, but it is missing a little part. See here we now have our previously passed parameters. But we do nothing with them. The idea here is to merge the previously passed parameters with the next parameters. What we will do now is unite the old parameters and the provided parameters.

function curry(callback, ...oldParameters) {
  return function(...parameters) {
    const nextParameters = [...oldParameters, ...parameters]

    if (nextParameters.length >= callback.length) {
      return callback(...nextParameters)
    }

    return curry(callback, ...nextParameters)
  }
}
        parameters   nextParameters
             /\            |
            /  \           |
           /    \          |
          /      \         | 
         /        \        |
         |        |        |
         v        v        v
greet("Hello", "ALLEN")("Barry")
                            parameters   
                                 /\     
                                /  \   
                               /    \ 
                              /      \ 
                             /        \
                             |        |
                             v        v
const greetAllens = greet("Hello", "ALLEN")


           nextParameters
               |
               |
               v
greetAllens("Barry")

And now we have a working curry function. We can use it on any functions.

function curry(callback, ...oldParameters) {
  return function(...parameters) {
    const nextParameters = [...oldParameters, ...parameters]

    if (nextParameters.length >= callback.length) {
      return callback(...nextParameters)
    }

    return curry(callback, ...nextParameters)
  }
}

const add = curry((a, b) => a + b)
const increment = add(1)
const decrement = add(-1)

console.log(add(1, 2)) // 3
console.log(increment(1)) // 2
console.log(decrement(1)) // 0

Conclusion

There are libraries that can take care of the implementation of a curry function such as Underscore.js or Lodash. What I showed you is one example of an implementation that can vary according to the needs or your own specifications.

This is not an easy topic and I may not have explained things properly. Please, if you have any question (there are no dumb questions) do ask me in the comment and I'll be glad to answer them.

Thank you for reading!

Discussion (0)

pic
Editor guide