This weekend I decided to try my hand at TypeScript. The goal I had was to implement a curry
function which can curry other functions with variable length arguments. Although this is a trivial exercise in JavaScript, i was interested in what it would take to implement a version in TypeScript which would be able to check the types of arguments of a curried function at compile time.
Background
What does it mean to curry a function?
Basically, if you have a function with the following signature (A function signature is a way of defining what a function ‘looks’ like. You can think of a function signature as a describing a function’s type. ):
function foo(a: string, b: string, c: number):string;
This signature tells us that our function, foo
takes 2 arguments of type string
and 1 argument of type number
, and returns something with type string
.
An example of a call to function foo
might look like this:
foo("bar","baz",123);
A curried version of the function foo
, Cfoo
, can be called as a chain of functions:
Cfoo("bar")("baz")(123);
i.e. A curried function returns function which takes one argument (or more), which will return a partially applied function which takes one argument(or more), and so-on, until the last function is called which in turn will call the underlying function (like we did to foo
, in the above example).
So, why would you want to curry anything?
It’s a pattern which encapsulates state. Let me give an example:
Let’s say you have a dbQuery API function with the following signature
// fake implementation of the API we have to work with...
function dbQuery(connStr:string,conf:Object,query:string):string;
To avoid having to write out the connection string and pass a config object around every time you want to make a query: you could take the Object-Orientated approach (where you need to implement a class which maintains the connection string and db config and which then exposes a query method, etc.) or, assuming you have a implementation of curryN
, by simply doing this in an app’s boot-up code:
// please don't ever hard-code auth details! This is an example only.
let query = curryN(dbQuery,3)("foo//bar/baz",{ usr: 'foo', pass: 'bar'});
This reusable function query
can be passed to the rest of your app. query
, will now implicitly have the signature:query(queryStr:string):string
(since the first 2 parameters were already partially applied immediately after calling the curryN
function). Note that the connection string and configObj are no longer needed: developers using this function would just focus on the query, and it is called just as you would a regular function:
// don't ever do this!
query("DROP * FROM TABLES;");
The above code is doing what the following equivalent, not very well written, TypeScript OO code would do:
// because other api's also need this...
class dbConnection {
connStr:string;
conf:Object;// Note I'm omitting this class definition for brevity...
constructor(connStr:string,conf:Object){
this.connStr = connStr;
this.conf = conf;
}
}
// because Bjarne Stroustrup told me it's cool to inherit
class query extends dbConnection {
constructor(connStr:string,conf:Object){
super(connStr,conf);
}
public query(query:string){
return dbQuery(this.connStr,this.conf,query);
}
}
/* in init code somewhere */
let query = new DbQuery("foo//bar/baz",{ usr: 'foo', pass: 'bar'});
// please don't ever hard-code auth details! This is an example only. You would assume that in 'real' code, the connStr and conf object would be configurable somewhere in a file never seen by git...
//don't do this..
query.query("DROP * FROM TABLES;");
In the following section you’ll see that a JavaScript implementation of curryN is more terse than the OO plumbing in the TypeScript code above.
Keep in mind that the above OO code will do essentially what the one-liner did when we just curried the API function.
Currying in JavaScript
Before we go on to the TypeScript implementation, let’s see how things work in plain old JavaScript.
General null terminated curry
implementation
Below is a generic JavaScript implementation of a variable-length-argument, null terminated curry
function (some type of termination is required for variable-length-argument curried functions).
function curry(f) {
return function () {
var args = Array.prototype.slice.call(arguments);
if (args.length)
return curry(f.bind.apply(f, [undefined].concat(args)));
return f();
};
}
General curryN
implementation
For currying known argument-length functions:
function curryN(f,n) {
return function () {
var args = Array.prototype.slice.call(arguments);
if (args.length<n){
return curryN(f.bind.apply(f, [undefined].concat(args)),n-args.length);
}
return f.apply(undefined,args);
};
}
We are employing three JavaScript tricks in the above two functions, curry
, and curryN
:
- The JavaScript way of converting an
arguments
array into a ‘real’ array (var args = Array.prototype.slice.call(arguments);
) is necessary to later use theArray.concat
method. - The JavaScript
bind
function performs a partial application of a function (exactly what we want for currying). We are recursively calling the curry function to return another function with the bound function until we run over the argument limit or get null terminated at which point we call the bound function. - We are also calling
apply
as a means to feed in an array of arguments as a list of arguments into the invoking function. - Eg.: If
args
was["Foo",1]
, thenf.apply(undefined,args);
invokesf
, as if it was written asf("Foo",1);
Note: Because JavaScript does not check function signatures and since superfluous arguments are ignored, the above implementation will work with any* JavaScript function as input (even other curried functions!).
*Not including native functions, like console.log… you have to wrap those around a variable-argument function yourself…
Note in
curryN
: less than is used in the checkif(args.length<n)
, to account for JavaScript not caring about superfluous arguments… otherwise it would beif(args.length==n)
The above implementations were done rather quickly since I have a familiarity with the listed tricks: a clear vindication of JavaScript’s advertised ability to do fast prototyping and a win for dynamic typing. ( If you’ve spent years learning the tricks, that is. )
To wrap up the JavaScript implementation:
Example usage of the curry
function with the example function foo
from the previous section:
var Cfoo = curry(foo);
Cfoo("bar")("baz")(123)(); // (null terminated)
Cfoo("bar")("baz",123)(); // equivalent behaviour to first call
Cfoo("bar","baz")(123)(); // equivalent behaviour to first call
Example usage of the curryN
function with the example function foo
from the previous section:
var Cfoo = curryN(foo,3);
Cfoo("bar")("baz")(123); // (curryN of foo)
Cfoo("bar")("baz",123); // equivalent behaviour to first call
Cfoo("bar","baz")(123); // equivalent behaviour to first call
The terseness and elegance of JavaScript is due to its untyped dynamic nature. Unfortunately this terseness and elegance also happens to infuriate those who want large-scale structure and mature tooling.
So: can we achieve at least the functional terseness but still have type information preserved?
Currying in TypeScript
But why?!
What is the advantage of doing the same thing in TypeScript?
Our one-liner query
function we produced earlier from curryN()
would be useless to a developer who didn’t write it and had to use it.
- Imagine: “
query
? Is this a URL query string or a database query object? I’ll have to see where it gets imported, look up that file (Since a jump-to-source isn’t available because there are 2 million definitions in the project of the stringquery
, grrr), and read the code… oh, its a data query function! …ok, now I can ignore it, because I was tasked to do something with the URL query string. F.M.L.”
Consider the nature of a truly dynamic language and how difficult it must be to produce any sort of decent auto-complete not driven off of some extra source of meta-data (like a YAML definition file or jsDocs you have to maintain separately.)
The tool-able OO plumbing code starts to look more inviting when we consider having to maintain a separate meta-data source just to convey the intent of what we’ve already implemented.
Can we can still go the functional route, and retain its benefits while getting the added benefit of conveying intent in type definitions? Might it still require less plumbing to get auto-complete on a curried function than going the OO route?
Answer: Well yeah, kinda, in one particular case…
Making it work
The problem with using the JavaScript curry
function implemented above in TypeScript is that any type information from the function being curried will be lost in the returned curried function. Can we overcome this limitation?
Note: You can use the TypeScript Playground to follow along.
One case immediately presents itself… functions which take a variable list of arguments of the same type, and return a single result of the same type. These classes of functions represent a large set of aggregate functions.
Eg.
concatStr();//'';
concatStr('bar','baz');//'barbaz';
concatStr('bar','baz','boo','baa');//'barbazboobaa';
bar(someObj1);//someNewObj;
bar(someObj1,someObj2,someObj3);//someNewObj;
They have a common signature which can be represented in a TypeScript function interface as follows:
interface aggregateFn<T> {
(...args: T[]): T;
}
This is what a function interface looks like in TypeScript. You can think of a function interface as a type definition for a set of function signatures. The ...
is called the “spread” operator and in this context it is a way you can refer to variable arguments in TypeScript as a typed array.
The generics notation <T>
is used to denote that the argument array’s content’s type and the return type are the same. You can think of a generic type as type-placeholder statement. We are keeping it simple here and sticking to one generic type.
A curried version of this class of function interface would have to be equivalent, except that it would also need to optionally return its’ own function interface:
interface curryFn<T> extends aggregateFn<T> {
(...args: T[]): curryFn<T>;
}
Note: Here you can see how TypeScript can deal with the
extends
keyword for function interfaces – meaning that the extended function interface inherits the characteristics of the interface it is extended from. ForcurryFn<T>
, the ‘compounded’ function signature set would now basically look like this:
(...args: any[]): curryFn<any> | any;
– The|
means ‘or’.
You can also see here how the compiler understands self-referential interfaces.
Finally, we have the pieces we need to implement our special class of typed curry
:
function curry<T>(f: aggregateFn<T>): curryFn<T> {
return (...args: any[]): curryFn<any> | any => {
if (args.length)
return curry(f.bind.apply(f,[undefined].concat(args)));
return f();
}
}
Looks pretty much the same as before – good thing, because you should understand most of it by now… but let’s examine the new type-related syntax…
In the function signature of the returned function:
(...args: any[]): curryFn<any> | any
The use of the any
keyword is bound by the return type in the function signature of the enclosing function:
function curry<T>(f: aggregateFn<T>): curryFn<T>
which in this case is curryFn<T>
.
Therefore, using the curry
function on a variable-argument length function should retain type information for the returned function, and all subsequent functions returned from those functions. This gives the TypeScript compiler enough information to determine if you are making type errors in chained functions, and subsequently makes your IDE more intelligent.
To test our new typed curry, let’s employ a typed test function:
/*
test function, just adds a list of numbers.
@params : variable list of numbers.
@returns: the numbers added.
*/
function addNumbers(...args: number[]): number {
return args.reduce((acc, itm) => acc += itm, 0)
}
To make a curried version is as simple as:
let curriedAdd = curry(addNumbers);
Note: The types are inferred.
Now, when we type the following incomplete statement into a TypeScript IDE:
curriedAdd(1)(2)(3)(4)(5)(
The auto-complete should kick in and let you know that you’re supposed to add numbers only (...args: number[])
. Maybe not helpful in this example, but very helpful if a more complex type of object was required at each call.
We’re not done
I leave it as an exercise to the reader to implement curryN<T>
in TypeScript.
(Hint: I’ve done most of the work for you)
I have only presented the TypeScript curry
solution for one set of functions.
Consider functions with this signature:
(a:fooObjType,b:any,c:barObjType,d:string,e:number,...args:any[]):bazObjType
The generic interface would have to look something like:
interface someFn<T1,T2,T3,T4,T5,R>{
(a:T1,b?:T2,c?:T3,d?:T4,e?:T5,...args:any[]):R;
}
Now start thinking of how you would implement a generalised curry of that… Yikes!
It becomes clear that for the dbQuery example before, even with 3 parameters, it makes more sense to just use a closed function which is made-for-purpose.
/*
function myQuery: queries the DB.
@param query: The db Query to run.
@returns: Db Query Result.
*/
let myQuery = ((connStr: string, conf: Object): (query: string) => string => {
return (query) => dbQuery(connStr, conf, query) ;
})("foo//bar/baz", { usr: 'foo', pass: 'bar' })
Note: The return type
(query: string) => string
requires=>
in a anonymous definition.
Now a 3-liner with no fancy-pants generics, not exactly worthy of much academic notice, but does the same job of encapsulating data and producing a reusable object, which happens to also be a function. Self-documenting and type-information-preserving. It is good to know that TypeScript allows us to choose the paradigm we feel is most suited for the job at hand.
Conclusion
Even though it was initially painful to approach TypeScript from a functional perspective, it soon became clear that the current incarnation of the TypeScript compiler (1.8 as of writing) is capable of handling the functional paradigm in a type-safe manner. I’m confident that we can port current solutions at our company that benefit from JavaScript’s functional capabilities to TypeScript. The exercise would be worth the pain for the tooling benefits, boosted collaborative efforts and improved maintainability.
By: Jaco Pretorius, Synthesis Software Technologies