Using ES6 Proxies for polymorphic functions

Grigorii Chudnov
4 min readFeb 4, 2016

--

ES6 introduces Proxy Objects to define and augment objects behavior. Proxy objects can be used for property validation, object access control, object virtualization and more. Proxies already implemented in Firefox, Edge and coming to Chrome 49.

Proxies can be used to implement polymorphic functions (multimethods). The exact function implementation is picked at runtime based on a special argument (dispatch value).

Multimethods

Multimethods is a language feature in Clojure. It is a combination of a dispatching function and one or more handler-functions. The dispatching function takes arguments supplied to a multimethod and produces a dispatch value. The dispatch value is used to find the associated handler-function. If one has been found, it is called with the provided arguments and result of that function becomes the result of the multimethod. If no handler-function is found, the multimethod looks for a function, associated with the default dispatching value and invokes it if present. Otherwise an error is triggered.

Not being supported by JavaScript directly, multimethods can be implemented with a little help of ES6 Proxies. Well, sort of. Multimethods in Clojure is a more powerful language feature with multiple dispatch and subtype polymorphism.

Example

As a cafe owner, you’re willing to remind customers to mind their manners. You’re planning to enforce the politeness policy and charge customers depending on their manners.

Using a switch statement, a coffee price policy might be enforced like this:

function coffeePrice(customer) {
switch (customer.manners) {
case 'polite':
return 2.00;
case 'rude':
return 6.00;
default:
return 3.00;
}
}

Serving some customers:

let alice = { name: 'Alice', manners: 'polite' };
let bob = { name: 'Bob', manners: 'rude' };

console.log('$' + coffeePrice(alice)); // Alice is polite, $2
console.log('$' + coffeePrice(bob)); // Bob was impolite, $6

To extend the policy and add a special price for customers that exaggerate their politeness, i.e. very polite, you have to alter the coffeePrice function and add another `case` there. Effectively we have a closed dispatch, you can’t add a new politeness type without rewriting coffeePrice or wrapping it.

Implementing using multimethods:

// maps a person to her manners
let dispatchFunc = person => person && person.manners;
// returns the default price
let
noMatchFunc = () => { return 3.0; };
let coffeePrice2 = createMultimethod(dispatchFunc, noMatchFunc);

Here we have a multimethod factory function, createMultimethod that takes a dispatching function, dispatchFunc and an optional default handler, noMatchFunc. The factory returns a new Proxy object that can be used to attach implementations to dispatching values and invoke the multimethod.

Multimethod Factory source code:

function createMultimethod(dispatch, noMatch) {
if(typeof dispatch !== 'function') {
throw new TypeError('dispatch must be a function');
}

const dict = {};
if(typeof noMatch == 'function') {
dict.noMatch = noMatch;
}

return new Proxy(() => { throw new Error('No match'); }, {
set(target, property, value) {
dict[property] = value;
return true;
},
apply(target, thisArg, args) {
const value = dispatch.apply(null, args);
const func = (value && dict.hasOwnProperty(value)
? dict[value]
: (dict.hasOwnProperty('noMatch')
? dict['noMatch']
: target));
return func.apply(thisArg, args);
}
});
}

The dictionary object, dict maps dispatching values to handlers. set is a trap for setting a property value, it is used to associate a dispatch value with a function-handler. apply is a trap for a function call, it looks up the handler and invokes it. Unlike the switch-statement, multimethod is extensible from the outside (open dispatch).

Enforcing policies:

coffeePrice2.polite = (person) => {
return 2.00;
};

coffeePrice2.rude = (person) => {
return 6.00;
};

Here, instead of setting a property value on an object, the proxy assigns the provided handler-function to the key (property name) in the dictionary.

Serving customers:

let alice = { name: 'Alice', manners: 'polite' };
let bob = { name: 'Bob', manners: 'rude' };

console.log('$' + coffeePrice2(alice)); // $2
console.log('$' + coffeePrice2(bob)); // $6

If we serve a customer without manners value, the default handler is used:

console.log('$' + coffeePrice2({})); // $3

Finally, to extend the policy and add a special price for customers that exaggerate their politeness, we add a new handler-function (extend multimethod from outside):

coffeePrice2.veryPolite = (person) => {
return 1.85;
};

And here is our new very polite customer, Carol:

let carol = { name: 'Carol', manners: 'veryPolite' };
console.log('$' + coffeePrice2(carol)); // $1.85

Conclusion

ES6 Proxies is a powerful meta-programming feature in JavaScript. Using Proxies we added a basic support for multimethods — using a single dispatch value to select a matching implementation. First, a multimethod is created, next, it is mutated with a new behavior. Arguments, provided to the multimethod are used by the dispatching function to make a single dispatch value and use it to select implementation.

Source code @ GitHub

--

--