Binding With Placeholders in Javascript
Function.prototype.bind
is incredibly useful for binding initial arguments, but sometimes we want to bind arguments at other indices. This post offers one simple implementation of binding with C++ inspired placeholders for Javascript.
Goals
- Argument agnostic.
- Support binding any function, including n-ary functions.
- Allow binding any number of arguments at arbitrary positions.
Motivating Example
Consider a function dot2
that calculates the two-dimensional dot product.
var dot2 = function(x1, y1, x2, y2) {
return (x1 * x2) + (y1 * y2);
};
Function.prototype.bind
can only bind initial arguments, such as binding x1
or x1
, y1
, and x2
. To bind arguments like y1
or x1
and x2
, we need to change dot2
. This is often accomplished by manually forwarding arguments using an anonymous function.
var dotForwarded = function(y1, y2) {
return dot2(10, y1, 10, y2);
};
dotForwarded(2, 3); // 106
Placeholders are a cleaner and clearer solution:
var dot10xs = placeholder(dot2, 10, _, 10, _);
dot10xs(2, 3); // dot2(10, 2, 10, 3) // 106
dot10xs(5, -1); // dot2(10, 5, 10, -1) // 95
placeholder(dot10xs, 5)(4, null, 2); // dot2(10, 5, 10, 4, null, 2) // 120
Implementation
placeholder
takes a function f
and a set of bound arguments. The bound arguments map argument indices to bound values, and may contains holes. For example, we can bind arguments at index 0 and 2, but leave index 1 open (along implicitly with all indices greater than 2). placeholder
returns a function that fills in unbound arguments and forwards all arguments to f
.
Bound arguments are provided as initial arguments to placeholder
. The index of a bound argument in the call to placeholder
determines the index of the argument in f
being bound.
We choose _
to identify a hole. The selection of _
is arbitrary, any identifier can be used but that identifier must identify a unique object. This ensures that all types of arguments, including falsy values and undefined
, are supported.
var _ = {};
One possible implementation of placeholder
is given here. This implementation uses iteration to merge the bound arguments and callee arguments, and Function.prototype.apply
to invoke f
.
var placeholder = function(f /*, ...*/) {
var bound = Array.prototype.slice.call(arguments, 1);
return function(/*...*/) {
var args = []; // Arguments to call f with.
// Copy all bound elements into position
for (var i = 0, len = bound.length; i < len; ++i)
if (bound[i] !== _) // Skip holes
args[i] = bound[i];
// Copy provided arguments into place
var indx = 0;
for (var i = 0, len = arguments.length; i < len; ++i) {
// Skip already bound positions
while (indx in args)
++indx;
// Add callee argument
args[indx++] = arguments[i];
}
// Call the function with the bound arguments.
//
// You can easily adapt `placeholder` to bind `this`
// like `Function.prototype.bind` but this implementation
// ignores `this`.
return f.apply(undefined, args);
};
};
Performance
This JSPerf shows placeholder
has high overhead compared to manually forwarding arguments. Every call to a function using placeholder
requires two function calls and a lot of iteration, array manipulation, and allocation.
Therefore, placeholder
is probably not a good solution for small math functions like dot2
. But for more substantial interfaces, placeholder
can make code cleaner and eliminate a lot manual argument forwarding.