Partial application and currying [EN]

ESPIAU FREDERIC
Sencrop Tech
Published in
7 min readDec 31, 2020

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 type number
  • the parameter b is of type string
  • areTheSame returns a boolean
  • 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 named a
  • the parameter a is of type number
  • the body of areTheSameInTwoFunctions will return a new anonymous function (which has no name)
  • this anonymous function declares a parameter namedb
  • the parameter b is of type string
  • 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 parameter
5 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, band 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 band 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 arguments
inc :: Num a => a -> a
inc b = add 1 b
inc = 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

--

--