Non-generic traps for non-generic objects (was: Overriding Map/etc with get/set hooks?)

Tab Atkins Jr. jackalmage at gmail.com
Fri May 24 09:55:01 PDT 2013


On Fri, May 24, 2013 at 7:02 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
> As I mentioned upstream, I don't think extending the Proxy API with hooks
> for accessing the private state of every possible built-in is going to fly.
>
> Here's a constructive counter-proposal:
>
> The difficulty of your use case is that you ideally would want to use
> inheritance to inherit all of Map's utility methods, but you are forced to
> use composition (i.e. wrapping) to avoid users applying the superclass
> methods directly onto your Map subclass.
>
> You could actually combine both techniques (inheritance and composition) to
> achieve the best of both worlds:
>
> // use inheritance to inherit Map's utilities
> class StringMap extends Map {
>   get(key) {
>     return super(String(key));
>   },
>   set(key, value) {
>     return super(String(key), String(value));
>   },
>   // similar for has, delete
> }
>
> // use composition via explicit forwarding to achieve protection against
> tampering with the StringMap instance directly:
> function createProtectedStringMap() {
>   var wrappedMap = new StringMap();
>   return new Proxy(wrappedMap, {
>     invoke: function(wrappedMap, name, args, thisBinding) {
>       return wrappedMap[name](...args);
>     }
>   });
> }
>
> var m = createProtectedStringMap();
> m.set(k,v) // wrappedMap.set(k,v)
> m.get(k) // wrappedMap.get(k)
> m.keys() // wrappedMap.keys()
>
> Map.prototype.set.call(m,k,v) // error: m is not a Map
> // even:
> StringMap.prototype.set.call(m,k,v) // error: m is not a Map
>
> It's not strictly necessary to use a Proxy here, since you could easily
> manually implement a forwarder object that defines all of the methods in the
> Map API. Using a Proxy has the advantage that if the Map API is extended
> later, the protected StringMap will support the extended API. Note though,
> that if in some later edition, the Map object suddenly defines a new method
> that allows alternative ways of getting/setting keys in the Map, the
> StringMap could be corrupted. You can't have it both ways: either you
> restrict the set of operations on your StringMap, which increases integrity,
> or you inherit an open-ended set of operations, which increases
> forward-compatibility.
>
> If you're going to spec the protected string map, think of it as an exotic
> object (aka host object) whose [[invoke]] operation explicitly forwards the
> request to a wrapped StringMap.
>
> (Note: the "invoke" trap used above was only just added to the Proxy API at
> this week's TC39 meeting, but you could implement this abstraction today by
> having the "get" trap returning a bound method)

What does the invoke trap do?  It looks like it somehow covers the
"p.foo()" case?  Can it handle "var f = p.foo.bind(p); f();" as well?
Just pointing me to docs would be sufficient, if they exist yet.

So, this is pretty clever.  It gives me most of what I want, which is
better than any suggestion so far.

The only bit I don't like is covered by your statement "You can't have
it both ways: either you restrict the set of operations on your
StringMap, which increases integrity, or you inherit an open-ended set
of operations, which increases forward-compatibility.".  In most
languages, I *can* have it both ways, because maps are parametric by
their very nature; the only difference between a string->string map
and an object->object map is the type declaration.

I simply don't understand why Javascript's Map apparently makes this
impossible, forcing all Maps to be any->any, and offering only hacks
(admittedly clever ones) that partially work if you want a restricted
type.

I mean, if I was a bad actor and just *specified* that the object was
a Map subclass but all attempts to set a value coerced both the key
and the value to strings, that would be it.  Simple and easy to
understand, and implementation wouldn't be hard either - it would be a
trivial change on the c++ side.  It's just here on the good-actor side
that it's hard, because Maps hop through proxies with their internal
[[MapData]] property.

It's just so simple and easy to do exactly what I want with an object
map, where setting "style.vars[obj1] = obj2;" coerces both objects to
strings and that's that.  I don't understand why it's supposed to be
so difficult for Maps, such that if I want the best solution I have to
open myself up to corruption. :/

~TJ


More information about the es-discuss mailing list