Non-generic traps for non-generic objects (was: Overriding Map/etc with get/set hooks?)
On Tue, May 21, 2013 at 4:07 AM, David Bruant <bruant.d at gmail.com> wrote:
David Bruant wrote:
Le 21/05/2013 04:06, Tab Atkins Jr. a écrit :
(One way to do this today is to subclass Map and provide my own get/set/etc. functions, but I need to override a potentially-open set (anything that doesn't directly lean on my overridden functions), and it doesn't prevent people from directly twiddling my [[MapData]] by calling Map.prototype.set.call() on my object.) If you want to keep control over how people interact with your key/value interface, a proxy seems more appropriate. It's been designed so that you have full control and can't be bypassed.
Although the behavior Map.prototype.set.call(someProxy) isn't very clear yet spec-wise [1] you can be sure that it won't be possible to freely mess around an internal [[MapData]] (because it would open an undesired communication channel)
Would it make sense to add specific traps for specific objects (Date would have some specific traps, so would objects with [[MapData]], so would objects with [[SetData]], etc.)? Very much like functions currently have some traps that only apply to them.
I'd be okay with this, if it lets me write something nice and simple in WebIDL like I can do today with getter/setter/etc to define the basic proxy operations. dev.w3.org/2006/webapi/WebIDL/#idl-named-properties
Something like a [MapClass] tag on the interface, which puts Map on the prototype chain, and mapgetter/mapsetter/mapcreator/mapdeleter special operations.
(We'd want to go ahead and introduce the same for Set while we're at it, obviously.)
2013/5/21 David Bruant <bruant.d at gmail.com>
Would it make sense to add specific traps for specific objects (Date would have some specific traps, so would objects with [[MapData]], so would objects with [[SetData]], etc.)? Very much like functions currently have some traps that only apply to them.
Yes, it would work in the current Proxy design. However, the function-specific "apply" and "construct" are arguably very generic and very frequently used operations.
I would be reluctant to add ad hoc traps for intercepting access to very specific operations on Date, Map, Set, WeakMap, etc. Moreover that design doesn't scale beyond the ES6 built-ins. Arguably other host objects might have other ops they want to intercept.
On Tue, May 21, 2013 at 4:07 AM, David Bruant <bruant.d at gmail.com> wrote:
David Bruant wrote:
Le 21/05/2013 04:06, Tab Atkins Jr. a écrit :
(One way to do this today is to subclass Map and provide my own get/set/etc. functions, but I need to override a potentially-open set (anything that doesn't directly lean on my overridden functions), and it doesn't prevent people from directly twiddling my [[MapData]] by calling Map.prototype.set.call() on my object.)
If you want to keep control over how people interact with your key/value interface, a proxy seems more appropriate. It's been designed so that you have full control and can't be bypassed.
Although the behavior Map.prototype.set.call(someProxy) isn't very clear yet spec-wise [1] you can be sure that it won't be possible to freely mess around an internal [[MapData]] (because it would open an undesired communication channel)
Would it make sense to add specific traps for specific objects (Date would have some specific traps, so would objects with [[MapData]], so would objects with [[SetData]], etc.)? Very much like functions currently have some traps that only apply to them.
With more thought, it seems like this, or something roughly equivalent to it, is indeed the correct solution.
It's impossible for me to reliably proxy Maps today. If I subclass
Map and provide my own implementations of set() and delete(), authors
can (accidentally?) work around them by calling
Map.prototype.set.call(style.vars, obj1, obj2)
and corrupt my map.
Even if the C++ side stringifies things (which, spec-wise, still
requires the kind of hooking that's been frowned upon in these
threads), I've got a confusing Map where two different keys both map
to the same CSS property, and I have to define that behavior somehow,
which is dumb.
(And of course, if I dont' subclass Map, then I'm in the unenviable position of reimplementing the entire Map API myself, including things that have nothing to do with my extra constraints like Map#keys(), and then having a static snapshot of the Map API stuck in my spec, which won't be automatically upgraded when the Map API grows. I don't consider this an acceptable outcome.)
So, yeah, I need a way to hook the specific internal Map operations somehow. I could do it with nothing more than Map-equivalents to the four special operations that WebIDL allows me to define.
I don't care if this is exposed as additions to the set of Proxy traps, or via special Symbols on the Map that are used for those operations, or something else.
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)
Map-like objects people will want include:
- something that looks like a Map, but backed by something other than [[MapData]], like DOM attributes, a JS object's properties, a hash table that doesn't retain entry order, or a key-value store;
- an observable Map;
- a Map that generates new entries, with default values, if you .get() a key that isn't in the map (cf Ruby Hash#default_proc);
- an immutable Map;
- a Map that type-checks values, as well as keys;
- a Map with different GC semantics.
Tab, is your present use case in CSS Variables (key coercion) the one common use case worth addressing? How would we know? Alternatively, can we cover all the likely needs by adding hooks?
Counterproposal: address this in WebIDL. Add a magic [Maplike] tag that means something like:
- the prose for this interface includes an algorithm for computing the list of this object's entries (as key-value pairs, like [[MapData]])
- the prototype for this interface has methods has, get, forEach, entries, keys, and values (and a .size accessor property), and they are defined in terms of that algorithm unless the prose says otherwise
- the prose for this interface includes algorithms for adding and deleting entries from this object
- the prototype for this interface has methods set, delete, and clear, defined in terms of that algorithm unless the prose says otherwise
It would be shorthand for making non-Map objects that walk and quack like a Map. From the spec author's perspective, this would eliminate most of the boilerplate, and it seems more flexible than hooks.
All the methods (and the .size getter) would be non-generic, so there would be no danger of one type of Map being corrupted by someone .apply()ing a method from another type of Map.
(Maybe this can be done with a Maplike interface and implements-statements instead—I don't really know WebIDL.)
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:
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. :/
On Fri, May 24, 2013 at 9:27 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
Map-like objects people will want include:
- something that looks like a Map, but backed by something other than [[MapData]], like DOM attributes, a JS object's properties, a hash table that doesn't retain entry order, or a key-value store;
- an observable Map;
- a Map that generates new entries, with default values, if you .get() a key that isn't in the map (cf Ruby Hash#default_proc);
- an immutable Map;
- a Map that type-checks values, as well as keys;
- a Map with different GC semantics.
Tab, is your present use case in CSS Variables (key coercion) the one common use case worth addressing? How would we know? Alternatively, can we cover all the likely needs by adding hooks?
I don't know about GC semantics (that sounds like something that really should be built in as something new, like WeakMap), but all the rest sound totally reasonable, and exactly the kinds of things that DOM and related APIs will want as they start using real Maps. All of those exist, today, as various object-based or ad-hoc Map-like APIs.
(Well, I don't know if there's any web API that does the "default value if you get a nonexistent key" thing, but I use defaultdict all the time in Python (and reinvented it in PHP for my own use), and if there was a way to do that in JS, I'd jump on it.)
Counterproposal: address this in WebIDL. Add a magic [Maplike] tag that means something like:
- the prose for this interface includes an algorithm for computing the list of this object's entries (as key-value pairs, like [[MapData]])
- the prototype for this interface has methods has, get, forEach, entries, keys, and values (and a .size accessor property), and they are defined in terms of that algorithm unless the prose says otherwise
- the prose for this interface includes algorithms for adding and deleting entries from this object
- the prototype for this interface has methods set, delete, and clear, defined in terms of that algorithm unless the prose says otherwise
It would be shorthand for making non-Map objects that walk and quack like a Map. From the spec author's perspective, this would eliminate most of the boilerplate, and it seems more flexible than hooks.
All the methods (and the .size getter) would be non-generic, so there would be no danger of one type of Map being corrupted by someone .apply()ing a method from another type of Map.
(Maybe this can be done with a Maplike interface and implements-statements instead—I don't really know WebIDL.)
This would also be completely acceptable, since it centralizes the "act like a Map" to WebIDL, rather than distributing it across every spec. It's just like today's getter/setter/etc keywords in WebIDL, which map to Proxy operations using some very simple spec prose.
The only additional detail I'd want is for Map to be on the prototype chain, so methods that authors add to Map show up on these objects as well. It just wouldn't run the Map constructor on the object, so it never sprouts a [[MapData]] that can be corrupted.
It's not the best solution, because the easy magic is only there for spec authors, but it's the best so far.
On 24 May 2013 18:55, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
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 haven't really followed this thread, but could it be that you are confusing polymorphic instantiation with subtyping here? A Map(String, String) is an instance of a polymorphic Map(A, B), but it is not a subtype of Map(Any, Any), because a map is contravariant in its key type. That's why the subclassing you have in mind does not really make sense from a subtyping perspective. If you want to restrict it, then you'd have to do so by wrapping, not by inheritance.
On Fri, May 24, 2013 at 10:11 AM, Andreas Rossberg <rossberg at google.com> wrote:
On 24 May 2013 18:55, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
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 haven't really followed this thread, but could it be that you are confusing polymorphic instantiation with subtyping here? A Map(String, String) is an instance of a polymorphic Map(A, B), but it is not a subtype of Map(Any, Any), because a map is contravariant in its key type. That's why the subclassing you have in mind does not really make sense from a subtyping perspective. If you want to restrict it, then you'd have to do so by wrapping, not by inheritance.
This is a reasonable argument, but there's no other way to express the right semantics in JS (this is a map, operations you add to general maps apply to this as well) without subtyping. If we ever come up with better solutions to handling OO, great, but I'm unhappy if we block any attempt to address Web API map-like needs just because it's not technically a subclass.
On Fri, May 24, 2013 at 12:02 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Fri, May 24, 2013 at 9:27 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
Counterproposal: address this in WebIDL. Add a magic [Maplike] tag that means something like: [...]
[...] It's not the best solution, because the easy magic is only there for spec authors, but it's the best so far.
I see your point. I think this is a case where spec authors definitely have a problem, but JS programmers will not sweat it that much.
In JS it's just so easy: gist.github.com/jorendorff/5645591
On Fri, May 24, 2013 at 11:53 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
On Fri, May 24, 2013 at 12:02 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, May 24, 2013 at 9:27 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
Counterproposal: address this in WebIDL. Add a magic [Maplike] tag that means something like: [...]
[...] It's not the best solution, because the easy magic is only there for
spec authors, but it's the best so far.
I see your point. I think this is a case where spec authors definitely have a problem, but JS programmers will not sweat it that much.
In JS it's just so easy: gist.github.com/jorendorff/5645591
That's only "easy" because we can assume that authors will manually adjust their code when we add more Map methods, or if they want to add their own Map methods. That's the exact thing I'm complaining about, except that we can't assume that specs will get updated in this way.
Another way to look at this is that there is no way to prevent a caller from using methods from the superclass on a subclass. In other OO languages, its much harder (or nearly impossible depending on the language) to forcibly call a superclass method against a subclass that has been overridden by the subclass.
Tab wouldn't have this issue if there were a way to prevent an external caller from executing Map.prototype.set.call(mapSubclass) if the mapSubclass overrides set
.
Ron
On Fri, May 24, 2013 at 2:35 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
Another way to look at this is that there is no way to prevent a caller from using methods from the superclass on a subclass. In other OO languages, its much harder (or nearly impossible depending on the language) to forcibly call a superclass method against a subclass that has been overridden by the subclass.
Tab wouldn't have this issue if there were a way to prevent an external caller from executing Map.prototype.set.call(mapSubclass) if the mapSubclass overrides
set
.
Sure, but that's attacking it on the wrong level, I think.
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it. If I could intercept access to [[MapData]] via proxy traps (like David suggests in the OP), everything would be perfect - every single problem I have would be resolved successfully, as would every additional possibility that Jason brings up.
If Maps used a Map.MapDataSymbol, I could intercept it via proxies by adding it to the symbol whitelist and do what I needed. Of course, the object represented by [[MapData]] is too abstract to actually do that, but we could expose some interactions for it.
On Fri, May 24, 2013 at 5:52 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, May 24, 2013 at 2:35 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
Another way to look at this is that there is no way to prevent a caller from using methods from the superclass on a subclass. In other OO languages, its much harder (or nearly impossible depending on the language) to forcibly call a superclass method against a subclass that has been overridden by the subclass.
Tab wouldn't have this issue if there were a way to prevent an external caller from executing Map.prototype.set.call(mapSubclass) if the mapSubclass overrides
set
.Sure, but that's attacking it on the wrong level, I think.
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it. If I could intercept access to [[MapData]] via proxy traps (like David suggests in the OP), everything would be perfect - every single problem I have would be resolved successfully, as would every additional possibility that Jason brings up.
Are you suggesting that the authors of JS abstractions should never use private state because then subclasses couldn't modify the behavior of that internal state?
On Fri, May 24, 2013 at 4:52 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it. If I could intercept access to [[MapData]] via proxy traps (like David suggests in the OP), everything would be perfect - every single problem I have would be resolved successfully, as would every additional possibility that Jason brings up.
I don't see how trapping access to [[MapData]] satisfactorily solves your original problem. What spec text would you write? Would you define the DOM object in question to be a proxy?
If Maps used a Map.MapDataSymbol, I could intercept it via proxies by
adding it to the symbol whitelist and do what I needed. Of course, the object represented by [[MapData]] is too abstract to actually do that, but we could expose some interactions for it.
But that would look like... Map.
So we would have turned Map into a wrapper around a [[MapData]] object that's what Maps are now (a concrete, non-hookable map).
It seems rather heavyweight. You can already get about the same effect just by writing that wrapper yourself.
On Fri, May 24, 2013 at 3:22 PM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:
On Fri, May 24, 2013 at 5:52 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Fri, May 24, 2013 at 2:35 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
Another way to look at this is that there is no way to prevent a caller from using methods from the superclass on a subclass. In other OO languages, its much harder (or nearly impossible depending on the language) to forcibly call a superclass method against a subclass that has been overridden by the subclass.
Tab wouldn't have this issue if there were a way to prevent an external caller from executing Map.prototype.set.call(mapSubclass) if the mapSubclass overrides
set
.Sure, but that's attacking it on the wrong level, I think.
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it. If I could intercept access to [[MapData]] via proxy traps (like David suggests in the OP), everything would be perfect - every single problem I have would be resolved successfully, as would every additional possibility that Jason brings up.
Are you suggesting that the authors of JS abstractions should never use private state because then subclasses couldn't modify the behavior of that internal state?
Kinda. ^_^ But no, in practice, private state is usually just fine. The problem here is that the "private" state in the Map makes it hard for other specs to define alternate types of maps.
On Fri, May 24, 2013 at 4:14 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
On Fri, May 24, 2013 at 4:52 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it. If I could intercept access to [[MapData]] via proxy traps (like David suggests in the OP), everything would be perfect - every single problem I have would be resolved successfully, as would every additional possibility that Jason brings up.
I don't see how trapping access to [[MapData]] satisfactorily solves your original problem. What spec text would you write? Would you define the DOM object in question to be a proxy?
Oh, easy. I'd define that, whenever you attempt to get or set something in the map, both the key and the value are coerced to strings before the operation proceeds. As well, whenever you set or delete something, the other data structure (the style rule) is updated accordingly.
Check out the spec in question: dev.w3.org/csswg/css-variables/#the-cssvariablesdeclaration-interface. I already have spec text that works perfectly, except that it's an "object map"; that is, you do "style.vars.foo = bar;", not "style.vars.set('foo', bar);".
I want to do exactly the same thing, except that instead of doing something every time you get/set/create/delete a property, I want to react to when you get/set/create/delete a key/value pair in the map. That way I use the soon-to-be-familiar Map API, and get free interop with any code that expects to manipulate Maps.
If Maps used a Map.MapDataSymbol, I could intercept it via proxies by adding it to the symbol whitelist and do what I needed. Of course, the object represented by [[MapData]] is too abstract to actually do that, but we could expose some interactions for it.
But that would look like... Map.
So we would have turned Map into a wrapper around a [[MapData]] object that's what Maps are now (a concrete, non-hookable map).
Kinda. If Maps had only four operations (get, set, delete, iterate) and I was guaranteed that that's all it would ever have, I'd have no problem just writing my own, since that's what I'll have to do in the spec anyway. That's what [[MapData]] kinda is - a simplified map with only a handful of basic operations.
The problem is that there are further Map functions that are built as relying directly on [[MapData]] rather than in terms of the "basic" operations, so to replace Map, I have to redefine those as well, even though the existing definition is perfectly adequate.
(When I wrote my own suite of collection classes, I did indeed define a "simplified map" that was the core of the real Map class, but couldn't be instantiated on its own.)
It seems rather heavyweight. You can already get about the same effect just by writing that wrapper yourself.
As I've argued and demonstrated in these threads already, no, you can't just do it yourself. Every solution fails in at least some details, usually trading security against convenience or the other way around. This isn't acceptable, given that I can produce a perfect "object map" with existing Proxy operations.
2013/5/24 Tab Atkins Jr. <jackalmage at gmail.com>
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.
Given that invoke() was only just now considered, I can't point you at any docs yet, but you got it right: invoke() handles the "p.foo()" case.
The example I posted can deal with "var f = p.foo.bind(p); f();" in the sense that this too will throw with an "error: not a map" when trying to apply one of the Map.prototype.* methods directly. If you want this to work reliably instead of throwing, you can have the proxy implement the "get" trap and have it return a bound function, binding |this| to the wrappedMap.
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.
Imagine we were doing all of this in Java: StringMap would be a class that extends from a platform-provided Map class, and overrides the necessary methods to add its own invariants. There's no need to use a wrapper because one cannot circumvent the subclass methods. But even then so: who is to say that my StringMap subclass remains safe when the JDK decides to add new methods to the Map class that should also be overridden by StringMap to enforce its additional constraints.
2013/5/24 Tab Atkins Jr. <jackalmage at gmail.com>
The problem is that the Map#set method grabs an internal property, bypassing Proxies, etc., so you can't defend against it.
Just to clarify, grabbing internal properties doesn't bypass proxies, not as currently specced. That's why Map.prototype.set.call(m,k,v) failed in my example upstream (where m is a proxy). The Map#set method will not bypass the proxy to reach into the wrapped object's [[MapData]].
I think what you meant is that accessing an internal property doesn't trigger any proxy traps, so you can't directly intercept it. That is true.
On Fri, May 24, 2013 at 8:28 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
On Fri, May 24, 2013 at 4:14 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
It seems rather heavyweight. You can already get about the same effect just by writing that wrapper yourself.
As I've argued and demonstrated in these threads already, no, you can't just do it yourself. Every solution fails in at least some details, usually trading security against convenience or the other way around.
Other axes include simplicity and performance, and there are tradeoffs with every proposed design. That’s how things are.
This isn't acceptable, given that I can produce a perfect
"object map" with existing Proxy operations.
Having one kind of hook does not imply that some other completely different kind of hook must be added. Software design is chock full of this kind of design tradeoff.
These assertions that a particular use case (your use case) must be addressed, in the particular way you've proposed, and that anything less is unacceptable—they are unconvincing, to say the least.
I still think WebIDL might be the way to go. After all it is WebIDL support that makes your current spec language possible. But I have one more possibly productive suggestion, which I'll try to post today.
On Tue, May 28, 2013 at 1:00 PM, Jason Orendorff <jason.orendorff at gmail.com>wrote:
I still think WebIDL might be the way to go. After all it is WebIDL support that makes your current spec language possible. But I have one more possibly productive suggestion, which I'll try to post today.
ES6 could provide a Mapping class, in a standard module, that works like this: gist.github.com/jorendorff/5662673
All those methods are generic. Map would be a subclass of Mapping, with its own fast non-generic methods shadowing the Mapping methods.
Benefits:
-
User-defined classes that wish to expose a map-like API can subclass Map (to inherit Map's implementation) or Mapping (to start fresh). Either way, they support all Map methods without having to implement them all.
-
New methods magically appear on all mappings when provided by the implementation (or by a polyfill).
-
No new hooks.
-
instanceof would work (within a window).
-
With minor WebIDL support, I think this satisfies Tab's use case of making the DOM's map-like objects feel more like Maps with minimal duplication of effort.
Some other languages do this. Python has collections.Mapping: docs.python.org/2/library/collections.html#collections-abstract-base-classes
Java has AbstractMap: docs.oracle.com/javase/6/docs/api/java/util/AbstractMap.html
But I think most languages do not provide anything like this; you just write your own.
On Tue, May 28, 2013 at 12:21 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
On Tue, May 28, 2013 at 1:00 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
I still think WebIDL might be the way to go. After all it is WebIDL support that makes your current spec language possible. But I have one more possibly productive suggestion, which I'll try to post today.
ES6 could provide a Mapping class, in a standard module, that works like this: gist.github.com/jorendorff/5662673
All those methods are generic. Map would be a subclass of Mapping, with its own fast non-generic methods shadowing the Mapping methods.
Benefits:
User-defined classes that wish to expose a map-like API can subclass Map (to inherit Map's implementation) or Mapping (to start fresh). Either way, they support all Map methods without having to implement them all.
New methods magically appear on all mappings when provided by the implementation (or by a polyfill).
No new hooks.
instanceof would work (within a window).
With minor WebIDL support, I think this satisfies Tab's use case of making the DOM's map-like objects feel more like Maps with minimal duplication of effort.
Some other languages do this. Python has collections.Mapping: docs.python.org/2/library/collections.html#collections-abstract-base-classes
Java has AbstractMap: docs.oracle.com/javase/6/docs/api/java/util/AbstractMap.html
But I think most languages do not provide anything like this; you just write your own.
This would be absolutely ideal. It would be cool if tc39 defined this; if not, WebIDL should be able to pick up the slack. It just seems useful for authors as well as specs.
On Tue, May 28, 2013 at 7:15 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:
ES6 could provide a Mapping class, in a standard module, that works like this: gist.github.com/jorendorff/5662673
All those methods are generic. Map would be a subclass of Mapping, with its own fast non-generic methods [...]
This would be absolutely ideal. It would be cool if tc39 defined this; if not, WebIDL should be able to pick up the slack. [...]
Is any TC39 member interested in putting this on the agenda for the next meeting?
If DOM really wants this, ES (not WebIDL) should provide it. WebIDL can't make Map a subclass of Mapping.
On Fri, May 31, 2013 at 8:05 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
On Tue, May 28, 2013 at 7:15 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
ES6 could provide a Mapping class, in a standard module, that works like this: gist.github.com/jorendorff/5662673
All those methods are generic. Map would be a subclass of Mapping, with its own fast non-generic methods [...]
This would be absolutely ideal. It would be cool if tc39 defined this; if not, WebIDL should be able to pick up the slack. [...]
Is any TC39 member interested in putting this on the agenda for the next meeting?
If DOM really wants this, ES (not WebIDL) should provide it. WebIDL can't make Map a subclass of Mapping.
No, but WebIDL can put Map on an object's prototype chain, and redefine all the secondary methods in terms of iterator/get/set/delete, which are defined by the spec for the interface.
This isn't absolutely ideal, and I'd appreciate this being done in ES, but it's a workable solution that I'm pursuing now in public-script-coord at w3.org.
Jason,
I don't think we have a consensus yet within TC39 WRT whether providing abstract classes of this sort is a good idea for JS, so I would be reluctant to push the matter to the committee at this time.
I don't really see why it even needs to addressed at the implementation level at this time. My reading of this thread is that Tab's major concern is that he wants to use in web API specs a "Map-like" interface that is guaranteed to have all of the generic map methods that ES specifies for Map.prototype. He doesn't want to explicitly list them because that would be a spec. maintenance hassle.
To me there seems to be a simple solution to that problem. Just specify, using prose, in the WebIDL spec. that the "Map-like" interface has all of the methods defined by the host implementation of Map.prototype. Since we are in spec-land the job is done...
Note that we have a similar issue in the ES6 spec. WRT Array.prototype methods and TypedArray. TC39 has already agreed that we want most of the Array methods to also be available as TypedArray methods. The current proposal accomplishes this by replicating the Array methods on a prototype that all TypedArray prototypes inherit from. However, a reasonable alternative that some members have suggested is to make TypeArrays inherit from Array.prototype. A less reasonable alternative is to actually refactor Array.prototype (mutating vs non-mutating methods, etc.). We will have to resolve this one way or another this summer.
On Fri, May 31, 2013 at 11:52 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>wrote:
I don't think we have a consensus yet within TC39 WRT whether providing abstract classes of this sort is a good idea for JS, so I would be reluctant to push the matter to the committee at this time.
I didn't mean to suggest the idea already had consensus.
Note that we have a similar issue in the ES6 spec. WRT Array.prototype
methods and TypedArray. [...] We will have to resolve this one way or another this summer.
Yes, that is a similar issue. Thanks, Allen.
David Bruant wrote:
Would it make sense to add specific traps for specific objects (Date would have some specific traps, so would objects with [[MapData]], so would objects with [[SetData]], etc.)? Very much like functions currently have some traps that only apply to them.