Spice up your Javascript with some powerful curry! (Functional Programming and Currying)
Mike Talbot ⭐
Posted on August 3, 2021
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
You could rewrite it as:
const calculate = a => b => c => (a * b) / c
You'd call the first one like this:
console.log(calculate(100, 20, 3))
And you'd call the second one like this:
console.log(calculate(100)(20)(3))
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))
}
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)
)
}
(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
)
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)
}
}
}
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))
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))
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")
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)
}
}
}
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 }
]
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>
)
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>
)
}
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>
)
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>
}
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>
)
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)
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>
)
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)
Posted on August 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.