Home & blog  /  2012  /  Feb  /  view post  /

A friendly alternative to currying

posted: 22 Feb '12 20:19 tags: curry, function, partial application, arguments

I recently did a small talk and demonstration on currying in JavaScript. My personal take is that, while occasionally useful, it's a bit of a nuclear option, for limited reward.

Er, what's currying?

If you don't know, currying involves what's called partial application: invoking functions or methods whilst omitting some of the arguments, for which default values are assumed instead.

In other languages this is simpler. In PHP, for example, you can specify default values at the point of declaring your function:

1<?php

2function someFunc($arg1 = 'default val') { echo $arg1; }

3someFunc(); //default val

4?>

Since that's not possible in JavaScript, the typical workaround is to feed your function to a curry-fier function, along with some default values, and get back a new version of your function that will assume default values for any omitted arguments. It might look something like this:

1//my simple function

2function add(num1, num2) { return num1 + num2; }

3    

4//a curry-fier function

5function curryfy(func) {

6     var slice = Array.prototype.slice,

7         default_args = slice.call(arguments, 1);

8     return function() {

9         var args = slice.call(arguments);

10         for (var i=0, len=default_args.length; i<len; i++)

11             if (default_args[i] && (args[i] === null || args[i] === undefined))

12                 args[i] = default_args[i];

13         return func.apply(this, args);

14     };

15}

16    

17//re-engineer our function to assume default values for omitted args

18var add = curryfy(add, 13, 14);

It's a little beyond the scope of this article to explain line-by-line what's going on there, but needless to say I can now call my add function with only some, or even no arguments.

1add(3, 4); //7

2add(); //27 - default values of 13 and 14 used

Drawbacks

That's all very nice. BUT, there are drawbacks and pitfalls to curry-fying. For one thing, a curryfied function no longer behaves as its originally-declared former self suggests. This might well throw a developer that comes to your code anew - particularly since JavaScript, as I say, does not support natively any notion of default function arguments.

Another problem concerns hoisting. If your function is a traditional one, i.e. as opposed to an anonymous function, the function itself will be hoisted, but its curryfied redefinition will not be (since curryfying a function always means declaring it as an anonymous function. For example:

1add(); //NaN - no values. Curryfied func doesn't exist yet.

2function add(num1, num2) { return num1 + num2; }

3var add = curryfy(add, 13, 14); //this time we redefine add as an anonymous function

4add(); //27

Our function is hoisted, since it's a traditional function not an anonymous one. That's the reason we can call it before the line where it's declared. However its curryfied redefinition will not be hoisted - so we would need to either curryfy it higher in the page or move our call to the function down.

A friendly alternative

My approach is rather different. Instead of redefining the function, let's ensure that it - and, indeed, every function - automatically inherits an extension to itself that allows us to omit variables. In addition, we'll set up another extension, called after the function is declared, that lets us set the default values. So the end result would be:

1function dog(breed, name) { alert(name+' is a '+breed); } //the func

2dog.setDefaults('poodle', 'Fido'); //set default value(s)

3dog('alsatian', 'Rex'); //call original version

4dog.withDefaults(null, 'Rex'); //call curryfied version

Hopefully it's clear what's happening there. I declare my function, then immediately stipulate what the default values are. Then, to invoke my function in such a way that I can have those default values substitute any omitted arguments, I call an extension of my function, called withDefaults(). To this I pass partial or even no arguments at all.

An advantage of this is that it doesn't destroy our original function, so anyone coming to our code anew and expecting traditional, not curryfied behaviour from our function, won't be surprised or confused.

Here's the code behind this approach:

1//function extension: allow version that uses default values

2Function.prototype.withDefaults = Function.withDefaults = function() {

3     var args = Array.prototype.slice.call(arguments);

4     if (this.defaults && this.defaults instanceof Object)

5         for (var i=0, len=this.defaults.length; i<len; i++)

6             if (this.defaults[i] && (args[i] === null || args[i] === undefined))

7                 args[i] = this.defaults[i];

8     this.apply(this, args);

9     return (function(outerThis) { return function() { outerThis.apply(this, args); }; })(this);(this);

10}

11    

12//function extension: set default values

13Function.prototype.setDefaults = function() { this.defaults = arguments; return this; }

As you can see, I'm declaring these two extensions on the Function object's prototype, so I can be sure they are both inherited by any and all functions I create.

I'll talk through the code in a separate blog post, but here's some usage demos for it, showing you different ways it can be used.

1//example 1: with traditional function

2function dog(name, breed) {

3     console.log(name+' is a '+breed);

4};

5dog.setDefaults(null, 'Daschund');

6dog.withDefaults('Fido');

7dog('Fido', 'spaniel');

8    

9//example 2: with anonymous function

10var dog = function(name, breed) {

11     console.log(name+' is a '+breed);

12}.setDefaults('Jeremy', 'Bassetthound');

13dog.withDefaults('Kevin');

14dog('Josh', 'Bassetthound');

15    

16//example 3: with instantiation

17var Dog = function (name, breed) {

18     this.name = name; this.breed = breed;

19}

20Dog.setDefaults('Russel', 'terrier');

21var dog1 = new (Dog.withDefaults());

22var dog2 = new Dog('Taz', 'labrador');

23console.log(dog1.name+' is a '+dog1.breed);

24console.log(dog2.name+' is a '+dog2.breed);

The output from that will be:

Example 1

1Fido is a Daschund

2Fido is a spaniel

Example 2

1Kevin is a Bassetthound

2Josh is a Bassetthound

Example 3

1Russel is a terrier

2Taz is a labrador

There's several interesting points of note there:

- When setting defaults for a function, you can specify values for as few or as many arguments as you like - it doesn't have to be all of them.

- When dealing with anonymous functions (example 2), you can call setDefaults() by chaining it to the end of your function literal.

- The approach even works with OO / instantiation, as shown in example 3. A lot of curryfyer scripts overlook this use case.

Phew. So, in summary?

You have a function. It accepts a lot of arguments. You find yourself passing many of the same arguments to it each time. Using the above script you can set some default values and then not have to worry about passing them as arguments when you call the function in future.

post new comment

Comments (0)