Understanding JavaScript Currying with a practical use case
Most used functional programming design pattern
The problem
Given a comma separated values string, aka “.csv”, with numbers in decimal system — base 10. Get a parsed array of integers.
Sounds like a good Interview question!
const input = '1, 2, 3, 4, 5'
function parseCSVNumbers(csvString){
// some logic :)
}
const output = [1, 2, 3, 4, 5]
A common solution
- Split csv input string, into an array of strings.
- Parse each string from the array.
- Return an array with the parsed numbers.
function parseCSVNumbers(csvString) {
const stringNumbers = csvString.split(',')
const parsedNumbers = stringNumbers.map(stringNumber => parseInt(stringNumber))
return parsedNumbers
}
Interview score: not bad…
This is one of the most used and simple currying in everyday JavaScript codes— even though it’s not commonly recognized as so:
stringNumber => parseInt(stringNumber)
With this extra explanation, our score was improved to good.
In step to really understand why this is in fact a valid currying case, let’s rewrite this callback as an ordinary JavaScript function:
function oneParameterParseInt (string) {
return parseInt(string)
}
The prefix oneParameter
was added on purpose to emphasize what this function really does — calls JavaScript standard built-in object parseInt
with strictly one parameter, and thus ensuring a radix/base of 10 parsing.
In other words, the arity of parseInt
was reduced from 2 to 1.
Currying by hand solution
Instead of limiting parseInt
to one parameter, let’s curry it properly:
function curriedParseInt(string) {
return function(radix) {
return parseInt(string, radix)
}
First call of curriedParseInt
which takes a string parameter, will return a function which takes a radix
parameter, which finally will call parseInt
with both parameters and return its result.
const parseOne = curriedParseInt('1')
const parsedNumber = parseOne(10)
or
const parsedNumber = curriedParseInt('1')(10)
Only our conceptual interview score was improved.
Well, is it possible to improve the solution using this curried version of parseInt
?
Short answer is no, because it’s still needed to explicitly provide the radix for each string value from the csv.
Invert parameters order
As the csv has the same radix for all its values, it’ll be useful to use the radix once and then to have a function to parse each value — a callback for the map.
This can be achieved by simply inverting the parameters order as follow:
function curryiedInvertedParseInt(radix) {
return function(string) {
return parseInt(string, radix)
}
Similarly, first call of curriedInvertedParseInt
which takes a radix
parameter, will return a function which takes a string parameter, which finally will call parseInt
with both parameters and return its result.
Finally it’s possible to improve the original solution as follow:
function parseCSVNumbers(csvString, base=10) {
const parse = radix => string => parseInt(string, radix) const stringNumbers = csvString.split(',')
const parsedNumbers = stringNumbers.map(parse(base))
return parsedNumbers
}
or
function parseCSVNumbers(csvString, base=10) {
const parse = radix => string => parseInt(string, radix)
const parseDecimal = parse(base) const stringNumbers = csvString.split(',')
const parsedNumbers = stringNumbers.map(parseDecimal)
return parsedNumbers
}
Our practical interview score has increased. Now we are very good!
Further improvements will require more functional base elements such as pipeline, compose or flow. Good news is there is a proposal for it https://github.com/tc39/proposal-pipeline-operator
A more generic currying solution
To reinforce currying concept, let’s code a generic function which takes any function and returns a curried version of it.
In the following recursive solution, bind is used — standard built-in function prototype method to set this
and to add preceding parameters.
function curry(fn) {
if (fn.length === 0) {
return fn()
}
return function bindOneParam (p) {
return curry(fn.bind(null, p))
}
}
A recursive function is a function that calls itself during its execution.
It’s very important to have a stop condition in recursion, which in this case is to have no parameters left from fn
— parameters length equal to 0.
This curry
version will extract one parameter at a time from fn
parameters, and return a new function which takes the extracted parameter as an input and binds it to fn
and call curry
again, but this time fn
has one less parameter. Last call will simply return fn
execution result.
Take your time to digest all these concepts.
Now it’s time to see how curry really works with the arity 2 — two arguments, parseInt
standard function:
const stringParam = curry(parseInt)
as parseInt.length
is not 0 , curry
will return stringParam
which is basically:
function stringParam (string) {
return curry(parseInt.bind(null, string))
}
Calling stringParam
with a string parameter:
const radixParam = stringParam('1234')
Will first evaluate the inner bind
function, which will extract one parameter from parseInt
and return a new function with ‘1234’ as a preceding argument:
const innerBind = parseInt.bind(null, '1234')
Then curry(innerBind)
, which is the first recursive call to curry
, but this time with the arity 1 innerBind
function instead of parseInt
, this will similarly return:
function radixParam (radix) {
return curry(innerBind.bind(null, radix))
}
Analog to stringParam
, calling radixParam
with a radix
parameter:
const decimalNumber = radixParam(10)
Will first evaluate the last bind
function, which will extract one parameter from innerBind
and return a new function with 10 as a preceding argument:
const lastBind = innerBind.bind(null, 10)
Then curry(lastBind)
, the second recursive call to curry
, but this time with the arity 0 lastBind
function instead of innerBind
, this will end the recursion, execute lastBind
and return its value.
If you still here, congratulation, our interview score now is excellent!
Curring is commonly used in modern JavaScript code, at least in its simpliest form, and combining it with pipe allow for a clean, readible and powerful functional programming — even in JavaScript.
Last interview question, why can’t we simply do this, without currying at all:
['1', '2', '3', '4'].map(parseInt)
?