JavaScript Function Metaprogramming

The JavaScript Function constructor is useful metaprogramming feature of JavaScript. It works like eval, treating the contents of a string as code, but without the quirkiness. The constructor’s API looks like this,

new Function([arg1[, arg2[, ... argN]],] functionBody)

For example, creating a 2-argument add function at run-time,

var add = new Function('x', 'y', 'return x + y');
add(3, 5);  // => 8

In all of the JavaScript engines I’m aware of, functions created this way are fully optimized and JITed just like any other, except that this is done later. The function isn’t established at compile-time but at some point during run-time. The constructor could be implemented in pure JavaScript using eval,

/* Notice: not 100% correct, but close. */
function Function() {
    var args = Array.prototype.slice.call(arguments, 0);
    var body = args.pop();
    return eval('(function(' + args.join(', ') + ') { ' + body + ' })');
}

Constructor Misuse

Misusing the Function constructor has the risk that you may invoke compilation repeatedly. For example, both of these functions return an array of adder functions.

function literal() {
    var out = [];
    for (var i = 0; i < 10; i++) {
        out.push(function(x, y) { return x + y; });
    }
    return out;
}

function constructor() {
    var out = [];
    for (var i = 0; i < 10; i++) {
        out.push(new Function('x', 'y', 'return x + y'));
    }
    return out;
}

The literal function creates 10 unique closure objects.

var x = literal();
x[0] === x[1];  // => false

While these appear to be unique objects, they’re all backed by the same code in memory. Since the function has no free variables, it doesn’t actually capture anything. These closures are really just empty handles to the same function. This function will be recognized and compiled ahead of time before literal is ever executed.

On the other hand, short of any serious optimization magic, the constructor function version of this creates 10 unique backing functions, invoking the compiler for each one individually at run-time. If they’re used enough to warrant it, each will also be optimized separately. This is a misuse of the Function constructor, as bad as misusing eval.

What’s it for?

As stated before, the Function constructor is useful for metaprogramming. Use it to generate source code programmatically. For example, this function generates 64 new functions by assembling source code from strings.

function opfuncs() {
    var ops = ['+', '-', '*', '/'];
    var names = ['a', 's', 'm', 'd'];
    var funcs = {};
    for (var i = 0; i < ops.length; i++) {
        for (var j = 0; j < ops.length; j++) {
            for (var k = 0; k < ops.length; k++) {
                var name = names[i] + names[j] + names[k],
                    body = ['w', ops[i], 'x', ops[j], 'y', ops[k], 'z'];
                funcs[name] = new Function('w', 'x', 'y', 'z',
                                           'return ' + body.join(''));
            }
        }
    }
    return funcs;
}

Writing out all these functions explicitly would take 66 lines of code instead of just 16, and it would be error prone and more difficult to maintain. Metaprogramming is a win here.

/* Ugh ... */
var opfuncs = {
    aaa: function(w, x, y, z) { return w + x + y + z; },
    aas: function(w, x, y, z) { return w + x + y - z; },
    /* ... */
    ddd: function(w, x, y, z) { return w / x / y / z; }
};

The opfuncs function should be called exactly once. These functions shouldn’t be generated multiple times because the benefits of the metaprogramming approach would be lost. To ensure that, I’m replacing the function with its result in this example,

opfuncs = opfuncs();
opfuncs.aaa(2,3,4,5); // 2+3+4+5 => 14
opfuncs.ama(2,3,4,5); // 2+3*4+5 => 19

The final metaprogrammed opfuncs object should completely indistinguishable from the longer, explicit version.

The Design Flaw

The primary flaw with the Function constructor is that it’s variadic. The whole purpose of this constructor is to dynamically generate functions at run-time, but there’s (generally) no straightforward way to call a constructor with a variable number of arguments. The apply method can’t directly compose with new.

Say we want to write a version of opfuncs where, rather than generate 64 4-argument functions, it generates 4^(n-1) n-argument functions, taking an argument n. Now new Function needs to be applied to a variable number of arguments (n + 1).

If rather than take argument names as individual arguments, Function took them as an array, this would be straightforward. It would be like having apply built-in. This is how I would have designed Function to work.

new Function(argNames, functionBody)

Used like this,

var add = new Function(['x', 'y'], 'return x + y');

Fortunately there’s a simple workaround. The built-in constructors do something useful for the most part when used without new. My personal favorite is the Boolean constructor. Without new it returns a primitive boolean based on the truthiness of its argument. It can be used to remove falsy values from an array.

[1, '', 'foo', 0, null].filter(Boolean);
// => [1, "foo"]

In the case of Function, new isn’t actually needed at all! This way, apply can be used with the constructor. This works as expected,

var add = Function.apply(null, ['x', 'y', 'return x + y']);

Here it is being used to generate functions of n arguments.

/** Return a function of n-args that sums its arguments. */
function addN(n) {
    var args = [];
    for (var i = 0; i < n; i++) {
        args.push('a' + i);
    }
    args.push('return ' + args.join(' + '));
    return Function.apply(null, args);
}

addN(5)(3, 4, 5, 6, 7);  // => 25

A single variadic function that uses the arguments special variable to sum its arguments could be used in place of these individual functions, but generating a function with a specific arity and using it many times will have much better performance than the variadic version.

function add() {
    var sum = 0;
    for (var i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

function test(f, n) {
    var start = Date.now();
    for (var i = 0; i < n; i++) {
        f(3, 4, 5, 6, 7);
    }
    return (Date.now() - start) / 1000;
}

test(add,     10000000);  // => 0.698 seconds
test(addN(5), 10000000);  // => 0.152 seconds

In MonkeyScript, the metaprogramming approach is almost 5 times faster.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)