DEV Community

Cover image for Spice up your Javascript with some powerful curry! (Functional Programming and Currying)
Mike Talbot ⭐
Mike Talbot ⭐

Posted on • Edited on

Spice up your Javascript with some powerful curry! (Functional Programming and Currying)

Functional programming and currying are topics that have some of us staring at the wall and saying something like "there is no spoon", whilst sadly shaking our heads. Yet we know that there is a powerful tool sitting there, so we struggle on in a bid for mastery of the dark arts.

I started life as a C/C++ programmer and over the years I've made money in a whole bunch of languages, but functional programming proved to be a very different path. I've come some way down this track, so I thought I'd share my understanding and one of the utilities I've made along the way.

Basics

Let's start with the basics.

If you have a function:

const calculate = (a, b, c) => (a * b) / c 
Enter fullscreen mode Exit fullscreen mode

You could rewrite it as:

const calculate = a => b => c => (a * b) / c
Enter fullscreen mode Exit fullscreen mode

You'd call the first one like this:

   console.log(calculate(100, 20, 3))
Enter fullscreen mode Exit fullscreen mode

And you'd call the second one like this:

   console.log(calculate(100)(20)(3))
Enter fullscreen mode Exit fullscreen mode

The second implementation is a function, which creates a function, which creates a function to calculate the answer (this is moving from The Matrix into Inception huh?)

We converted the original using Javascript arrow functions and basically replacing a, with a =>. The first function returns takes the parameter a and returns a function for the parameter b. Thanks to closures the final function has access to all of the previous parameters and so can complete its work.

The benefit of this is code reuse. Until the last function we are basically running a factory to create functions that have the already supplied parameters baked in.

  const calculateTheAnswer = calculate(100)(20)
  for(let i = 1; i < 1000; i++) {
     console.log(calculateTheAnswer(i))
  }
Enter fullscreen mode Exit fullscreen mode

Now in this case you might be saying "oh nice, seems ok, can't see the point though". The strength comes when you start making more complicated things by passing functions around as parameters and "composing" solutions out of multiple functions. Lets take a look.

Currying

For the sake of this article I want an example that is simple, yet not only "multiplying two numbers together". So I've come up with one that involves multiplying and taking away ;) Seriously though, I hope the that it proves to give a practical perspective.

Ok, so imagine we are building a website for a manufacturing company and we've been tasked with displaying the weights of the company's "UberStorage" containers when made in a variety of sizes and materials.

Some smart bloke has provided us with access to a library function to calculate the weight of a unit.

function weightOfHollowBox(
    edgeThickness,
    heightInM,
    widthInM,
    depthInM,
    densityInCm3
) {
    return (
        heightInM * widthInM * depthInM * (densityInCm3 * 1000) -
        (heightInM - edgeThickness * 2) *
            (widthInM - edgeThickness * 2) *
            (depthInM - edgeThickness * 2) *
            (densityInCm3 * 1000)
    )
}
Enter fullscreen mode Exit fullscreen mode

(See multiplying and taking away). We don't want to mess with this as it isn't our code and might change, but we can rely on the "contract" of the parameters being passed.

Our website is going to need to display lots of different output like this:

So we are going to have to iterate over dimensions and materials and produce some output.

We want to write the minimum code possible, so we think of functional programming and curry!

Firstly we could make up a wrapper to that function:

const getHollowBoxWeight = (edgeThickness) => (heightInM) => (widthInM) => (
    depthInM
) => (densityInCm3) =>
    weightOfHollowBox(
        edgeThickness,
        heightInM,
        widthInM,
        depthInM,
        densityInCm3
    )
Enter fullscreen mode Exit fullscreen mode

But immediately we start to see some problems, we have to call the functions in the right order, and given our problem we need to think hard to see if we can make up a perfect order that maximises reuse. Should we put density first? That's a property of the material. edgeThickness is standard for most of our products so we could put that first. Etc etc. What about the last parameter, we probably want that to be the thing we iterate over, but we are iterating both material and dimensions. Hmmmm.

You might be fine writing a few versions of the wrapper function, you might be fine throwing the towel in saying "I'll just call weightOfHollowBox" but there is another option. Use a curry maker to convert the weightOfHollowBox to a curried function.

Simple curry, not too many ingredients

Ok so a simple curry function would take weightOfHollowBox as a parameter and return a function that can be called with a number of the arguments. If we have completed all of them, calculate the weight, otherwise return a function that needs the remaining parameters. Such a wrapper would look a bit like this:

const currySimple = (fn, ...provided) => {
    // fn.length is the number of parameters before
    // the first one with a default value
    const length = fn.length
    // Return a function that takes parameters
    return (...params) => {
        // Combine any parameters we had before with the
        // new ones
        const all = [...provided, ...params]

        // If we have enough parameters, call the fn
        // otherwise return a new function that knows
        // about the already passed params
        if (all.length >= length) {
            return fn(...all)
        } else {
            return currySimple(fn, ...all)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If we call this on weightOfHollowBox we end up with a function that is a little more flexible than the hand written one:

   const getWeightOfBox = currySimple(weightOfHollowBox)

   // All of these combinations work
   console.log(getWeightOfBox(0.1)(10)(10)(3)(.124))
   console.log(getWeightOfBox(0.1, 10, 10)(3)(.124))
Enter fullscreen mode Exit fullscreen mode

We can pass all of the parameters or any subset and it works in those cases. This does not solve our parameter ordering issue. We would dearly love a version of this that allowed us to miss out interim parameters and have a function for just those.

e.g.

   const getWeightOfBox = curry(weightOfHollowBox)
   const varyByWidth = getWeightOfBox(0.1, 10, MISSING, 3, .124)
   console.log(varyByWidth(4))
Enter fullscreen mode Exit fullscreen mode

Jalfrezi

Warning there follows some much more advanced code to create this new curry function - you don't need to understand it if you don't want to. You could use this implementation or one of the many others out there without needing to get the inner workings. If you want to see how this is done read on, otherwise skip to the next section.

Ok lets cook up some proper curry. First we need something that uniquely identifies a missing parameter.

const MISSING = Symbol("Missing")
Enter fullscreen mode Exit fullscreen mode

With that in our toolbox, we can go ahead and write our new curry function.

const curry = (
    fn,
    missingParameters = Array.from({ length: fn.length }, (_, i) => i),
    parameters = []
) => {
    return (...params) => {
        // Keeps a track of the values we haven't supplied yet
        const missing = [...missingParameters]
        // Keeps a track of the values we have supplied
        const values = [...parameters]

        // Loop through the new parameters
        let scan = 0
        for (let parameter of params) {
            // If it is missing move on
            if (parameter === MISSING) {
                scan++
                continue
            }
            // Update the value and the missing list
            values[missing[scan] ?? values.length] = parameter
            missing.splice(scan, 1)
        }
        // Call the function when we have enough params
        if (missing.length <= 0) {
            return fn(...values)
        } else {
            // Curry again? Yes please
            return curry(fn, missing, values)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Right, let's start with those parameters. The fn is the function to be curried, the next two we use when recursing through in the case that we need to make another intermediate function rather than call fn. missingParameters defaults to the numbers 0..n where n is the number of parameters required by fn - 1. In other words, when we first call it, it is the indices of all of the parameters required for fn. The next parameter is an empty array that we will populate and pass down should we need to.

The function we return takes any number of parameters. We take a copy of the missing indices and the existing parameters and then we iterate over the new parameters. If the parameter value is MISSING we move on to the next missing index. When it isn't MISSING we populate the correct index in the values array (which we allow to take more parameters than the function, as that's how you deal with any that might have been defaulted). Having populated the array we remove the missing index.

Once that's all done, if the missing list is empty then we call the function, passing it the values, otherwise we recurse.

Note: we never set the length of the array, Javascript arrays automatically set their length to the maximum value if you write to an index in them.

That's it, this function allows us to create a range of templates.

Example Web Site

Now we have a way of wrapping weightOfHollowBox we can start to put together the elements of our web page.

Firstly lets code up the thing that shows the weight of an item and its material. We can see that the inner item is something based on iterating over the material. We have this definition of materials:

const materials = [
    { name: "Aluminium", density: 2.71 },
    { name: "Steel", density: 7.7 },
    { name: "Oak", density: 0.73 }
]
Enter fullscreen mode Exit fullscreen mode

So we write a curried function to render the item that takes a way to calculate the weight (a function we will create from our curried weightOfHollowBox) and a material:

const material = (weightInKg) => (material) => (
    <ListItem key={material.name}>
        <ListItemText
            primary={material.name}
            secondary={
                <span>
                    {(weightInKg(material.density) / 1000).toFixed(1)} tons
                </span>
            }
        />
    </ListItem>
)
Enter fullscreen mode Exit fullscreen mode

This will display any material so long as we can give it a function to calculate the weight that requires the density.

Let me show you a simple way this could now be used:

function Simple() {
    const weightInKg = curriedWeight(0.05, 10, 3, 3)
    return (
        <List className="App">
            {materials.map(material(weightInKg))}
        </List>
    )
}
Enter fullscreen mode Exit fullscreen mode

We create a weight calculator looking for density and then we call our material function, passing that, which returns a function that needs a material, this will be passed by the materials.map().

We are going to do something fancier for the site though.

A block for all materials

We want to output a list of materials so let's write a function for that.


const materialBlock = (header) => (weightCalculator) => (
    materials
) => (dimension) => (
    <Fragment key={dimension}>
        {header(dimension)}
        {materials.map(material(weightCalculator(dimension)))}
    </Fragment>
)
Enter fullscreen mode Exit fullscreen mode

This curried function allows us to supply something that will write a header, then given a weight calculator, a list of materials and a dimension it will output all of the materials for that group.

That's a bit trickier, let's see how we might use that in an isolated way:

const ShowByHeight = () => {
    const heights = [2, 3, 5, 10]
    const weightCalculator = curriedWeight(0.05, MISSING, 5, 3)
    const outputter = materialBlock((height) => (
        <ListSubheader>5 m wide x {height} m tall</ListSubheader>
    ))(weightCalculator)(materials)
    return <List className="App">{heights.map(outputter)}</List>
}
Enter fullscreen mode Exit fullscreen mode

Here we have a React component that knows the standard heights of our units. It creates a weight calculator that still requires height and density and then provides materialBlock with a header to put over it.

For the site we can get better code reuse though!

const ShowBy = (weightCalculator) => (header) => (values) => (
    <List className="App">
        {values.map(
            materialBlock(header)(weightCalculator)(materials)
        )}
    </List>
)
Enter fullscreen mode Exit fullscreen mode

We create a reusable ShowBy function, which we can then use to create versions for our standard widths and heights.

const widths = [1, 4, 7, 10]
const heights = [2, 3, 5, 10]

const ByWidth = () =>
    ShowBy(curriedWeight(0.05, 10, MISSING, 3))((width) => (
        <ListSubheader>10 m tall x {width} m wide</ListSubheader>
    ))(widths)

const ByHeight = () =>
    ShowBy(curriedWeight(0.05, MISSING, 5, 3))((height) => (
        <ListSubheader>5 m wide x {height} m tall</ListSubheader>
    ))(heights)
Enter fullscreen mode Exit fullscreen mode

Pulling it together

Our final function is used to put the parts together:


const Advanced = () => (
    <Box>
        <Box mb={2}>
            <Card>
                <CardHeader title="By Width" />
                <CardContent>
                    <ByWidth />
                </CardContent>
            </Card>
        </Box>
        <Box mb={2}>
            <Card>
                <CardHeader title="By Height" />
                <CardContent>
                    <ByHeight />
                </CardContent>
            </Card>
        </Box>
    </Box>
)
Enter fullscreen mode Exit fullscreen mode

Here's the whole thing:

Conclusion

I hope this has been an interesting look at currying in Javascript. The area of functional programming is very deep and we've only scratched the surface, but there exist here some techniques that are practical to use in many scenarios.

Thanks for reading!

(All code MIT licensed)

Top comments (7)

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer

I still struggle to see the benefit of currying. To me as a front-end focused developer (mostly doing CSS and not much JS) currying just seems to replace one style of hard-to-read syntax with another, instead of using configuration objects with named parameters?

Collapse
 
miketalbot profile image
Mike Talbot ⭐

I kinda agree - I have on a few occasions found it useful to be able to return functions with baked in parameters, but on the whole, I don't use it much still.

Collapse
 
abhishekraj272 profile image
Abhishek Raj

After reading this post, I remember, I was asked in an interview to create a function to calculate sum of N numbers using currying, it was really good solving it being a begineer.

If you want to have a good concept over curriying do try this problem.

Fun(1)(2)(3)...(N) = 1 + 2 + 3 + ... + N
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jamesthomson profile image
James Thomson

Great write up, very informative and I appreciate the simple, yet not so simple example to really drive home currying.

Just one correction/typo, I believe your currySimple function should be returning itself in the last statement rather than return curry(fn, ...all) - which I'm assuming came from your advanced example.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Thanks for the spot - I renamed the function (which I'd called curry when I wrote it) and not the recursion! :)

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

My free js-coroutines library supports functional programming (amongst many other things) while the browser is idle! Lose the jank...

Collapse
 
artydev profile image
artydev

Glad to encounter the author of this awesome library :-)