Implementing Multiple Value Returns in JavaScript

October 8th, 2010

Before discussing possible implementations of multiple value returns, I want to illustrate some of the use cases for them.

Let's start with something simple. The function get treats obj as a hash table or dictionary and returns the value of indexing it by key.

function get (key, obj) {
    return obj[key];
}

At first, everything seems ok...

get("foo", { foo: 5}); // 5

...however, ambiguities begin to arise. Ignoring the issues with prototypes, .hasOwnProperty(), and modifications to the global object's prototype, how can we tell the difference between when a key is explicitly set to undefined, and when there is no value associated with that key and it returns undefined by default?

get("foo", {}); // undefined
get("foo", { foo: undefined }); // undefined

Here is another example. Taking the User API for granted, the following getOrCreateUser function also has some serious issues. Is it a new user or not? The caller of this function probably needs to know that information.

function getOrCreateUser (username, email) {
    var user = User.query.byUsername(username)
                         .byEmail(email)
                         .first();
    return user !== null
        ? user
        : new User({
            username : username,
            email    : email
        });
}

Both of these types of issues can be solved with multiple return values where the secondary return values contain meta information about the primary return value. Unlike languages such as Common Lisp and Lua, JavaScript does not natively support multiple return values, however we have a variety of ways we can implement them ourselves.

Return multiple values as an array

It is fairly common to see multiple values returned as an array; sometimes I have returned objects to accomplish the same thing. There are advantages and drawbacks to this technique, and it is incredibly more useful with destructuring assignment (a watered down flavor of pattern matching) than without.

function getOrCreateUser (username, email) {
    var user  = User.query.byUsername(username)
                          .byEmail(email)
                          .first(),
        isNew = user === null;
    if ( isNew )
        user = new User({
            username : username,
            email    : email
        });
    return [user, isNew];
}

This technique works fairly well if you are only supporting Mozilla's JS 1.7 and up, and therefore can rely on destructuring assignment.

var user, isNew;

[user, isNew] = getOrCreateUser("franky", "frankster@gmail.com");
console.log(user); // [object Object]
console.log(isNew); // true

[user, isNew] = getOrCreateUser("franky", "frankster@gmail.com");
console.log(user); // [object Object]
console.log(isNew); // false

If you need to support the various browsers and their various JavaScript/ECMAScript implementations, arrays become clumsy. The caller is forced to destructure the return values by hand.

var temp  = getOrCreateUser("dolemite", "rudy@thetotalexperience.com"),
    user  = temp[0],
    isNew = temp[1];

The abstraction is leaky because it forces the caller of the function to know that it returns multiple values via an array; you can't just get the primary value via a normal invocation. Suppose that in one case, we don't care whether it is a new user or not.

Someone who is a consumer of our getOrCreateUser API might just assume it returns one value; this will lead to bugs.

var user = getOrCreateUser("mocha", "kitty@cat.com"); // Wrong; leads to bugs!

Once they have tracked down their nasty bug created above, they will find that the multiple values returned as an array is a leaky abstraction.

var user = getOrCreateUser("mocha", "kitty@cat.com")[0]; // Leaky abstraction!

Returning an object also suffers from the same ailments, but without taking advantage of any destructuring syntactic sugar in JS 1.7.

// Gross
var temp  = getOrCreateUser("dolemite", "rudy@thetotalexperience.com"),
    user  = temp.user,
    isNew = temp.isNew;

// Leads to bugs: return value is a container object not a user.
var user = getOrCreateUser("odb", "dirtdawg@gmail.com");

// Leaky abstraction
var user = getOrCreateUser("vinnie", "vincent.chase@entourage.com").user;

Continuation Passing Style

We can fix some of these issues with Continuation Passing Style. (For those who are not familiar with CPS, it basically involves passing around callback functions that represent the computation waiting to happen when the current function returns). If we want to return more than one value, it is as simple as calling the continuation with more than one argument.

function get (key, obj, cont) {
    return cont(obj[key], key in obj);
}

get("foo", {}, function (val, hasProp) {
    console.log(val); // undefined
    console.log(hasProp); // false
});

get("foo", { foo: undefined }, function (val, hasProp) {
    console.log(val); // undefined
    console.log(hasProp); // true
});

But now we are forced to use functions for every invocation, even when we only want the primary return value. (Granted, it is nice that JS has such dynamic function parameters which let us choose to bind only some of the returned values).

console.log(100 === get("foo", { foo: 10 }, function (val) {
    return val * val;
}));
// true

This does not seem to be that much better than the previous solution involving arrays; it certainly is not more concise. The best we can do is to make the continuation optional and if it is missing just return the primary value.

function get (key, obj, cont) {
    return typeof cont === "function"
        ? cont(obj[key], key in obj)
        : obj[key];
}

get("foo", { foo: "Normal" }); // "Normal"

get("foo", {}, function (val, hasProp) {
    console.log(val); // undefined
    console.log(hasProp); // false
});

The API for the caller of this function is excellent — the best that JavaScript can offer in this situation — but the bit of logic that checks for the continuation leaves something to be desired. Do we need to repeat this snippet for every function that will return multiple values? Of course not.

The values function

Formalizing the pattern of optional continuations, we can create a function values which takes a continuation followed by the values to return. If the continuation is not a function (for example it is undefined because it was not passed to the caller function) then the primary return value is returned.

function values (k /*, and values */) {
    return typeof k === "function"
        ? k.apply(this, Array.prototype.slice.call(arguments, 1))
        : arguments[1];
}

Here is the final revision of the getOrCreateUser function, which uses values.

function getOrCreateUser (username, email, cont) {
    var user  = User.query.byUsername(username)
                          .byEmail(email)
                          .first(),
        isNew = user === null;
    if ( isNew )
        user = new User({
            username : username,
            email    : email
        });
    return values(cont, user, isNew);
}

Maybe we don't care whether or not the user is new.

var me = getOrCreateUser("fitzgen", "fitzgen@gmail.com");

Or maybe we have special things to do with new users before continuing.

var me = getOrCreateUser("fitzgen", "fitzgen@gmail.com", function (user, isNew) {
    if ( isNew )
        createPasswordFor(user);
    return user;
});

Conclusion

The values function is the best of all available options. You can call the function normally as if it only returned one value without passing in a continuation function, or you can capture all (or as many as you want) of the return values via the continuation. It is also compatible with all JavaScript/ECMAScript environments.

Thanks to Kartik Agaram, Angus Croll, and Brian Goldman for reading drafts of this article.

« Previous Entry

Next Entry »


Recent Entries

Naming `eval` Scripts with the `//# sourceURL` Directive on December 5th, 2014

wu.js 2.0 on August 7th, 2014

Come work with me on Firefox Developer Tools on July 8th, 2014

Debugging Web Performance with Firefox DevTools - Velocity 2014 on June 26th, 2014

Beyond Source Maps on March 12th, 2014

Memory Tooling in Firefox Developer Tools in 2014 on March 4th, 2014

Hiding Implementation Details with ECMAScript 6 WeakMaps on January 13th, 2014

Re-evaluate Individual Functions in Firefox Developer Tools' Scratchpad on November 22nd, 2013

Testing Source Maps on October 2nd, 2013

Destructuring Assignment in ECMAScript 6 on August 15th, 2013

Creative Commons License

Fork me on GitHub