Auto-Currying In TypeScript
Long awaited type-safe implementation of the auto-currying function in TypeScript. No bullshit.
Origin Story
One interesting feature of "truly functional" languages like Haskell or, God forgive, ReasonML is out-of-the-box auto-currying. I'd not want to explain what the currying means and why it's additionally being referred to as "auto" – you can go ahead and read about it yourself somewhere else. Or even check it out in the aforementioned languages. Just gonna lay out the basic example of it here (what's also called partial application):
const add = (a: number) => (b: number): number => a + b;
console.log([1, 2, 3, 4, 5].map(add(1))); // [2, 3, 4, 5, 6]
Cool, huh?
What's not cool, is that it is rather different to express syntactically. Additional measures have to be taken for the partial application to work automatically, like doing the currying manually (the part where we return another function of b
).
In Haskell, the example above looks much easier and does not require any excess moves:
add :: Int -> Int -> Int
add = (+) -- yup, just the plus would be enough for this weirdo
do putStrLn $ show $ map (add 1) [1, 2, 3, 4, 5] -- [2, 3, 4, 5, 6]
Here, we'd not even need the separate
add
function as just themap (+ 1)
would work just fine, too.
The add
function above does not necessarily have to be partially applied, it works just well "without" it:
add 2 2 -- 4
"Without" is quoted because in Haskell the partial application is done by default. For example, add 2
returns a new function that takes in an Int
and returns an Int
as a result. There's just no additional syntax involved.
On the contrary, in JavaScript, we'd have to specify what would be curried and what would be not. That's why, after learning what Haskell could do just out of the box, I could not rest until I'd find out the way to do the same in JavaScript.
Automatic Partial Application in JavaScript
What we need in JS is literally a higher order function (HOF) to wrap our function of choice and create a new one out of it. This new function will return a function each time we have some arguments left that we did not apply yet. How do we know the amount of arguments we have "left"? Pretty easily – length
property of a function will tell us exactly that. What it won't tell us, though, is the amount of arguments when there are optional ones among them, or they are defined using the spread syntax (the function is variadic, like console.log
). Yikes!
Fear not, lads, we're covered. Why not just tell our HOF to curry by the length
and, if it's not possible to determine it programmatically, to rely on the manually specified amount?
Okay, let's quickly sketch out the implementation:
function curry (fn, length = fn.length) {
return (...args) => {
const argsLength = args.length;
if (argsLength === length) {
return fn(...args);
}
if (argsLength > length) {
return fn(...args.slice(0, length)); // make sure to not pass more arguments than needed
}
return curry(
(...nextArgs) => fn(...args.concat(nextArgs)),
length - argsLength
);
};
}
Basic JavaScript here, nothing fancy.
Now, let's try to apply this curry
guy to our previous add
function and improve the developer experience (DX) a bit:
const add = curry((a, b) => a + b);
[1, 2, 3, 4, 5].map(add(1)); // [2, 3, 4, 5, 6]
add(2, 2); // 4
What's significant here – we can as easily apply both arguments at once, or just one of them. It just works both ways.
Using the length
argument is just as easy:
function sum (...args) {
return args.reduce((a, c) => a + c, 0);
}
const sumOfThree = curry(sum, 3);
sumOfThree(2)(3)(5); // 10
There are two downsides to it:
Non-commutative operations, like subtraction, will not work as expected. What do you think of when seeing
subtract(1)
? I see something liken - 1
. But, actually, the opposite happens –1 - n
. This is the real problem which can not be solved by thecurry
itself, and I'd like to cover it sometime in the future in a separate post.!remindme 2 months
This function is not typed. Well, fair enough, we're in JavaScript where no types exist. But what if we'd want to use our fancy
curry
in the TypeScript land? That would not be as easy as we think.
Automatic Partial Application in TypeScript
So what's exactly the problem with TypeScript?
First, how do we type the partial application itself? We could use the Parameters
type at least, but how to make it partial?
Second, how do we tell the compiler that every application could end up with either a new function being returned or with an end result of the initial function?
I tried to find the solution on the internet – nothing even good enough for me. I tried building it myself – zilch. That's when the Gradual
type kicked in.
The Gradual
Type
Whilst lurking the internet for the solutions other people have come up with, I've found the one that stood out to me – https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f. Take 15 minutes and read this article, it's actually a good one.
At first, it may look irrelevant. Where's the auto currying in here? But we're not looking for it in this article. We seek the BuildTuple
type that reignited my imagination and made the cogs spinning again.
The following applies to TypeScript >=4.2.2.
It shows the [...T]
type that gave me a hint – what if we could build a type that creates a union of any possible tuple lengths?
type Gradual<T extends any[] | readonly any[]> = T extends [...infer R, any]
? R['length'] extends 0
? T
: T | Gradual<R>
: T;
Gradual<[1, 2, 3]>; // [1] | [1, 2] | [1, 2, 3]
Does this look like a partial application to you 😏?
With this in our tool belt, we're ready to tell the compiler about the possible amount of arguments we're expecting to receive in this very "round" of application. When there are still arguments left, we continue with the currying. When there are none – return the result. But how do we know there are "none"?
Literal Generic Inference
Yeah, I've come up with the term myself just now.
This topic is basic and easy, but still a crucial part to our cool typing.
When we receive the function as an argument to our curry
, it magically has a length
property on it. Turns out, we can obtain its literal value in a type and store it inside a curry
function in a form of generic. And we're not even gonna need to annotate it explicitly – it just comes out of our function, or is specified as the length
argument manually.
With all of this in mind, let's take a look at this beauty:
export type Currying<
Function extends VariadicFunction,
Length extends number = Parameters<Function>['length']
> =
<Args extends Gradual<Parameters<Function>>>(...args: Args) =>
Args['length'] extends Length
? ReturnType<Function>
: Currying<
(
...args: Slice<Parameters<Function>, Args['length']>
) => ReturnType<Function>
>
function curry<
Function extends (...args: any[]) => any,
Length extends number = Parameters<Function>['length']
> (
fn: Function,
length = fn.length as Length
): Currying<Function, Length> {
// the implementation is identical to the JS version
}
The Currying
type does exactly what I've described above – returns a new function for every "not enough" (less than Length
) amount of arguments, and returns a result when we're "done".
Helper Slice type here just slices the tuple from the end for a specified amount.
In the curry
definition we see this "literal generic inference" in full power: fn.length as Length
tells the compiler to treat the fn.length
as a literal and store it in the Length
generic type. When the fn.length
is not possible – the literal value suffices just as well.
The type information stored in the curried function is the same we see in the Gradual
type:
curry(sum, 3);
/* Either of the following:
(arg_0: number, arg_1: number, arg_2: number) => number;
(arg_0: number) => (arg_1: number, arg_2: number) => number;
(arg_0: number, arg_1: number) => (arg_2: number) => number;
(arg_0: number) => (arg_1: number) => (arg_2: number) => number;
*/
The only problem I see is that sometimes it does not reliably save the names of the arguments and replaces them with this weird arg_n
construct. It's just the visuals, though, the functionality is perfectly fine still.
The Library
Now, when our beautiful curry
function shines in its fullest, why not make a library with it? Genius thought! That's exactly what I've done!
Please, allow me to quickly introduce fnts
to y'all. It has many cool things aside form curry
which I'll tell about in some other posts.
For now, let's say you're free to check it out 😎