Partial application and currying [EN]
Preamble
This article uses TypeScript
but the concepts discussed are applicable to many languages
Here is a short explanation of TypeScript
syntax
const areTheSame = (a: number, b: string): boolean => a.toString() === b;
This instruction
- creates a function called
areTheSame
areTheSame
declares two parameters a and b- the parameter
a
is of typenumber
- the parameter
b
is of typestring
areTheSame
returns aboolean
- the part behind the
=>
is the body of the function
It is possible to create functions that return functions in this way
const areTheSameInTwoFunctions = (a: number) => (b: string): boolean => a.toString() === b;
This instruction
- creates a function called
areTheSameInTwoFunctions
- the
areTheSameInTwoFunctions
function declares a parameter nameda
- the parameter
a
is of typenumber
- the body of
areTheSameInTwoFunctions
will return a new anonymous function (which has no name) - this anonymous function declares a parameter named
b
- the parameter
b
is of typestring
- the anonymous function returns a
boolean
Both functions can be run as follows
const theyAreTheSame = areTheSame(1, "1");
const theyAreNotTheSame = areTheSameInTwoFunctions(1)("2");
Difference between parameter and argument
A parameter
is found in the declaration of a function to indicate what data it requires to be executed
An argument
is a concrete value passed to a parameter
const increment = (a: number): number => a + 1;
increment(5);
a
is a parameter5
is an argument
Definition of the term arity
Number of arguments a function needs to be executed
const addThree = (a: number, b: number, c: number): number => a + b + c;
The addThree
function needs values assigned to its 3 parameters a
, b
and c
to be executed
const six = addThree(1, 2, 3);
Thus, its arity is 3
Definition of the term unary
Function whose arity is 1
const increment = (n: number): number => n + 1;
increment
needs a value assigned to its parameter n
to be executed
const two = increment(1);
Thus, its arity is 1
Definition of partial application
To generate a new function B
from a function A
. The arity of function B
must be lower than the arity of function A
const addThreeNumbers = (a: number, b: number, c: number): number => a + b + c;
const addTenToTwoNumbers = (b: number, c: number): number => addThreeNumbers(10, b, c);
addThreeNumbers
has an arity of 3
addTenToTwoNumbers
, created from addThreeNumbers
, has an arity of 2
It is therefore partial application
Giving a value to a parameter in order to perform a partial application is called a fix
or a bind
In JavaScript
and TypeScript
, it is possible to partially apply a function with bind
const addTenToTwoNumbers = (b: number, c: number): number => addThreeNumbers.bind(null, 10);
This technique has the disadvantage of forcing you to provide a context for your new function (null
in the previous example) and only works with the first argument of your function (it would not have been possible to create a partial application on b
or c
)
This is why in JavaScript
and TypeScript
, we more often find functions that return other functions to perform a partial application, or functions dedicated to the partial application as we will see later on
What’s the point?
A function partially applied has some of its parameters set and is waiting for one or more arguments to be executed
Therefore, a partially applied function is a function that contains information
This function can then be used to create new functions, with information already set as an argument
Therefore, a partially applied function is able to create new functions that already contain information
const buildUrl = (protocol: string, domain: string, path: string) => protocol + "://" + domain + "/" + path;
const buildSecureUrl = (domain: string, path: string): string => buildUrl("https", domain, path);
I can reuse buildSecureUrl
to build values or functions that will share the fact that the protocol to use is https
const buildWebsiteUrl = (path: string): string => buildSecureUrl("website.com", path);
const buildAppUrl = (path: string): string => buildSecureUrl("app.website.com", path);
const urlToInternshipOffer = buildSecureUrl("careers.website.com", "offer?id=10");
Partial application is thus used to avoid repetition or to avoid making expensive calculations several times
const myFunction = (arg1, arg2) => {
const complexResult = functionThatTakesTime(arg1);
return complexResult + arg2;
}const otherFunction = myFunction(1, 3);
const thirdFunction = myFunction(1, 2);
The time-consuming calculation is performed twice, whereas with partial application, it could be performed only once
const myFunction = (arg1) => {
const complexResult = functionThatTakesTime(arg1);
return (arg2) => complexResult + arg2;
}const alreadyDone = myFunction(1);
const otherFunction = alreadyDone(3);
const thirdFunction = alreadyDone(2);
The time-consuming calculation is performed only once
One could go through an intermediate variable that would contain the result, but this would be to deprive oneself of the composability of the functions, a vast subject that goes beyond the scope of this article
Thus, the main interest of the partial application is to allow the creation of functions that will allow the creation of other functions
Definition of the term currying
Let’s take the example of the following function
const add = (a: number, b: number): number => a + b;
add
is not unary because its arity is 2
Currying add
would mean
- get from
add
a new function - which would perform the same action as
add
- which would be unary
- which would return a function that is itself currified to handle the remaining arguments
const add = (a: number, b: number): number => a + b;
const addCurried = (a: number) => (b: number): number => a + b;
The addCurried
function is a unary function, it has as only argument a
It returns an anonymous unary function, which has as sole argument b
and performs the initial computation
const five = addCurried(2)(5);
Thus, a currified function
- takes a single argument
- returns either a currified function or the expected result
What’s the point?
A curried function is a partially applied function whose behavior you know in advance
No need to worry about the number of arguments to provide, it will always take a single argument and return a function that will take one as well, or a final result
Thus, if you create a currified function, you know that it is waiting for an argument
- either to return a currified function
- either for it to be executed
In computer science, knowing what something looks like for sure is to be able to perform very powerful actions on it
const rawIds = ["id-35", "id-53", "id-657"];
const numericPartOfMyIds = rawIds.map(id => id.split("-")[1]);
I know in advance what my identifiers look like, they are strings of characters that start with id-
and then contain a number
So I can easily extract the digital part with a simple split
const rawIds = ["29437", 32, "id-15"];
With this convention of identifiers there, it is much more complicated for me to extract the numerical part
The same principle of predictability applies for the functions
As a function returns only one value, if another function takes only one argument, then I can easily chain the result of a function to supply it as a parameter to another function
const increment = (_: number): number => _ + 1;
const isOver = (boundary: number) => (toTest: number): boolean => toTest > boundary;[-2, 2, 3]
.map(increment)
.filter(isOver(0))
The increment
, isOver
and isOver(0)
functions are very easy to reuse in other contexts
This would not be the case with a function that takes more than one argument, as I would need a value for the parameters that are not the first one
const add = (a: number, b: number): number => a + b[-2, 2, 3]
.map(/* I can't use "add" directly here */)
Thus, the main interest of curryfication is to allow the creation of functions that will allow the creation of other functions, easy to use without knowing them in advance
Autocurrying
Manually writing unary functions to curry a function can be time consuming
const addFiveNumbers = (a: number) => (b: number) => (c: number) => (d: number) => (e: number): number => a + b + c + d + e;
There are libraries that perform this action manually, for example Ramda
const addFiveNumbers = R.curry(a: number, b: number, c: number, d: number, e: number): number => a + b + c + d + e);
const addTenToNumber = addFiveNumbers(1, 2, 3, 6);
const twelve = addTenToNumber(2);
Note that this function does not return a new curried function but a function that allows partial application in a very flexible way
const addTenToNumber = addFiveNumbers(1, 2)(3, 6);
Currying in other languages
Functional oriented languages try to reproduce the behaviour of mathematical functions
However, a mathematical function can have only one argument
Therefore, functions in some functional languages can have only one parameter
It is possible to declare functions with several parameters but the compiler will automatically curry the function
let add a b = a + b
This example in F#
shows a function called add
that has two parameters a
and b
to add them together
At compile time, the generated function will look something like this
let add a =
let addSubFuction b =
a + b
addSubFuction // returns the inner function
It is then possible to use the function as follows
let inc b = add 1 b // creation of an "add" function with two arguments
let incOther = add 1 // one can also write the declaration of "incOther" thus, the "b" is implicit
let three = add 1 2
let four = inc 3
The same reasoning applies in Haskell
add :: Num a => a -> a -> a
add a b = a + b -- creation of an "add" function with two argumentsinc :: Num a => a -> a
inc b = add 1 binc = add 1 -- one can also write the declaration of "incOther" thus, the "b" is implicit
Conclusion
Partial application and currying are two mechanisms for creating specialized functions from generic functions
They allow you to have less duplication and improve the readability of your code
Even better, by using functional-oriented development techniques, they make it possible to write code by exploiting the composability of functions, especially currying