Overriding Map/etc with get/set hooks?

# Tab Atkins Jr. (5 years ago)

For the CSS Variables spec I need to define an object with arbitrary string keys, with the initial set determined by the custom properties set in the style rule, and on modification I need to coerce the provided key to a string, and then go mutate the style rule accordingly. When the style rule is mutated to add/remove custom properties, I also need to mutate the exposed keys on the object. (In other words, this object has a bidirectional link with a style rule; it just exposes a more convenient and specialized interface for custom properties specifically.)

Right now I'm defining this via the WebIDL getter/setter/etc hooks, which ends up defining an "object map". This is bad practice, though, because anything set on the prototype chain will show up as a (non-own) key, potentially causing confusion.

I'd like to convert this over to an ES Map, as that avoids the above issue and gets me all the Map extras for free, which is nice. However, I don't think it's currently possible to do what I need.

Is it possible add appropriate hooks to the ES spec to let me define the [[MapData]] via a spec, rather than as an initially-empty list of tuples that get/set unconditionally read from? I need to be able to define in spec-ese that the [[MapData]] tuples consist of some list of data from a style rule, and that whenever a value gets set, I first coerce the key to a string, and then go mutate the style rule instead of the [[MapData]] (it then picks up the mutated data by virtue of being defined by the style rule).

(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.)

Alternately: Proxies? Is that possible? What benefits/drawbacks come from that?

# Domenic Denicola (5 years ago)

Seems like this isn't really a Map? It'd be pretty confusing for something to pretend to be a Map but act in such coercive and side-effecty ways. Better to just obey the Map structural type, perhaps, to give people an interface they're used to while not pretending to be something you're not?

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 7:55 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

Seems like this isn't really a Map? It'd be pretty confusing for something to pretend to be a Map but act in such coercive and side-effecty ways. Better to just obey the Map structural type, perhaps, to give people an interface they're used to while not pretending to be something you're not?

Oh, no, "obeying the Map structural type" is much worse than even my bad solution of subclassing Map in my OP. It means I have to define bespoke versions of all the Map functions myself, rather than just the "basic" ones (and keep adding to the interface as ES adds to Map), functions added by the author to Map.prototype don't show up, it doesn't type as a Map with the ES methods of typing, etc.

It's clearly a Map - it's a set of key/value tuples, which you can create/read/update/delete/iterate, exactly like the vanilla Map. The only difference is that its [[MapData]], rather than being a freshly-created independent empty list upon creation, is a spec-defined list that reflects the data from another object. This doesn't interfere with its operation as a Map, it just makes the get/set operations slightly more complex than they are for vanilla maps.

# Domenic Denicola (5 years ago)

Oh, I must have misread your original message. I thought it did not allow storing non-string keys. If it can allow storing any kind of key, like a Map, and it's just the initial data you're referring to, then maybe it is a Map. As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 6:20 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

I think it would have to subclass those methods by using toString() on the argument passed. It seems that would still be Map-like though.

-- annevankesteren.nl

# Domenic Denicola (5 years ago)

On May 21, 2013, at 1:23, "Anne van Kesteren" <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 6:20 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

I think it would have to subclass those methods by using toString() on the argument passed. It seems that would still be Map-like though.

Hmm, so that invariant wouldn't hold? I assume has would be similarly broken? Seems? not so Map like, besides perhaps having some operations with the same name as Map's.

Relevant: Liskov substitution principle

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 10:20 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

Oh, I must have misread your original message. I thought it did not allow storing non-string keys. If it can allow storing any kind of key, like a Map, and it's just the initial data you're referring to, then maybe it is a Map. As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

It's a string-keyed map, but that just means that you toString everything, as Anne says. Your contract is still maintained, assuming a non-degenerate toString.

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 10:27 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

Hmm, so that invariant wouldn't hold? I assume has would be similarly broken? Seems… not so Map like, besides perhaps having some operations with the same name as Map's.

Relevant: en.wikipedia.org/wiki/Liskov_substitution_principle

Let's ignore Liskov; I don't really care about subclassing, not least because being strictly Liskov-pure makes most subclasses invalid.

The things I care about:

  • when someone asks "is this Map-like?" in an appropriately idiomatic JS way, they get a "yes" answer.
  • when someone adds a new function to Maps in an appropriately idiomatic JS way, the method also applies to this object
  • when JS expands the set of built-in methods for Map, it also gets applied to this object without me having to update my spec
  • for all the existing Map methods, I get identical/equivalent methods without having to manually redefine every single one of them

All of these are easy to do if this is just a Map (or has Map on its prototype chain), but with a custom [[MapData]] whose behavior is defined by my spec.

# Domenic Denicola (5 years ago)

On May 21, 2013, at 1:28, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:

On Mon, May 20, 2013 at 10:20 PM, Domenic Denicola Is<domenic at domenicdenicola.com> wrote:

Oh, I must have misread your original message. I thought it did not allow storing non-string keys. If it can allow storing any kind of key, like a Map, and it's just the initial data you're referring to, then maybe it is a Map. As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

It's a string-keyed map, but that just means that you toString everything, as Anne says. Your contract is still maintained, assuming a non-degenerate toString.

Right, I guess it's this contract that gets broken: x !== y implies map.set(x, 1); map.set(y, 2); map.get(x) === 1; map.get(y) === 2.

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 10:33 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

On May 21, 2013, at 1:28, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:

On Mon, May 20, 2013 at 10:20 PM, Domenic Denicola Is<domenic at domenicdenicola.com> wrote:

Oh, I must have misread your original message. I thought it did not allow storing non-string keys. If it can allow storing any kind of key, like a Map, and it's just the initial data you're referring to, then maybe it is a Map. As long as the contract that map.set(x, y); map.get(x) === y works for any x and y, then you're probably fine.

It's a string-keyed map, but that just means that you toString everything, as Anne says. Your contract is still maintained, assuming a non-degenerate toString.

Right, I guess it's this contract that gets broken: x !== y implies map.set(x, 1); map.set(y, 2); map.get(x) === 1; map.get(y) === 2.

Yes, because it's a string map rather than an object map. But that difference doesn't justify breaking all the qualities I listed in my previous message.

# Sam Tobin-Hochstadt (5 years ago)

On Mon, May 20, 2013 at 10:32 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

All of these are easy to do if this is just a Map (or has Map on its prototype chain), but with a custom [[MapData]] whose behavior is defined by my spec.

Would another way to think about this be as a regular plain-old Map, but which is updated imperatively by the environment sometimes, and is also read by the environment? Or does changing the map also change some part of the style of the page immediately, rather than at the end of the turn?

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 11:10 PM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

On Mon, May 20, 2013 at 10:32 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

All of these are easy to do if this is just a Map (or has Map on its prototype chain), but with a custom [[MapData]] whose behavior is defined by my spec.

Would another way to think about this be as a regular plain-old Map, but which is updated imperatively by the environment sometimes, and is also read by the environment?

Sure, that's also valid.

Or does changing the map also change some part of the style of the page immediately, rather than at the end of the turn?

It would be best if it happened immediately. There's no performance-related reason not to make it sync, and high potential for confusion if you use one method to write it, another tool uses the other method to read it, and they don't sync up. It's just a (necessary) convenience API for reading/writing the CSS properties directly.

# Sam Tobin-Hochstadt (5 years ago)

On Mon, May 20, 2013 at 11:17 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, May 20, 2013 at 11:10 PM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

On Mon, May 20, 2013 at 10:32 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

All of these are easy to do if this is just a Map (or has Map on its prototype chain), but with a custom [[MapData]] whose behavior is defined by my spec.

Would another way to think about this be as a regular plain-old Map, but which is updated imperatively by the environment sometimes, and is also read by the environment?

Sure, that's also valid.

Or does changing the map also change some part of the style of the page immediately, rather than at the end of the turn?

It would be best if it happened immediately. There's no performance-related reason not to make it sync, and high potential for confusion if you use one method to write it, another tool uses the other method to read it, and they don't sync up. It's just a (necessary) convenience API for reading/writing the CSS properties directly.

Is it possible for the environment to change the Map during the turn?

IOW, is this always true?

m.set("x", 1); assert(m.get("x") === 1);

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 7:24 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Is it possible for the environment to change the Map during the turn?

IOW, is this always true?

m.set("x", 1); assert(m.get("x") === 1);

If we ignore the toString() and other validation, yes. But note that setting will directly be observable through getComputedStyle. Object.observe does not work for this, just like it does not work for URLQuery (which we discussed a while back).

-- annevankesteren.nl

# Sam Tobin-Hochstadt (5 years ago)

On Mon, May 20, 2013 at 11:32 PM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 7:24 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Is it possible for the environment to change the Map during the turn?

IOW, is this always true?

m.set("x", 1); assert(m.get("x") === 1);

If we ignore the toString() and other validation, yes. But note that setting will directly be observable through getComputedStyle. Object.observe does not work for this, just like it does not work for URLQuery (which we discussed a while back).

If that's all the case, then why do you need any fancy spec methodology? You have a Map, which is consulted by, and mutated by, the environment. When you first create this Map, you add a bunch of stuff to it. And that's it, AFAICT.

# Tab Atkins Jr. (5 years ago)

On Mon, May 20, 2013 at 11:24 PM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Is it possible for the environment to change the Map during the turn?

IOW, is this always true?

m.set("x", 1); assert(m.get("x") === 1);

Your example is not a restatement of your question.

Yes, the "environment" can change the map during the turn. But the map isn't affected by arbitrary things in the "environment" - it just shares its internal data with another object which is also user read/writeable. The only way the map can change without a .set() call is if your code does some CSSOM manipulation that changes the custom properties in the associated style rule.

# Sam Tobin-Hochstadt (5 years ago)

On Mon, May 20, 2013 at 11:58 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, May 20, 2013 at 11:24 PM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Is it possible for the environment to change the Map during the turn?

IOW, is this always true?

m.set("x", 1); assert(m.get("x") === 1);

Your example is not a restatement of your question.

Yes, the "environment" can change the map during the turn. But the map isn't affected by arbitrary things in the "environment" - it just shares its internal data with another object which is also user read/writeable. The only way the map can change without a .set() call is if your code does some CSSOM manipulation that changes the custom properties in the associated style rule.

Ok, that's what I thought was going on. We can make some other function calls which might mutate the Map, just as with any other Map that we get from someone else. In that case, my response to Anne stands -- why is any spec magic needed at all?

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 8:11 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Ok, that's what I thought was going on. We can make some other function calls which might mutate the Map, just as with any other Map that we get from someone else. In that case, my response to Anne stands -- why is any spec magic needed at all?

Well basically, you want an object that's a Map for all intents and purposes, but does a couple of things differently. So e.g. you need a custom set and all Map operations that use set need to use that new set. Tab's original message explains that however as far as I can tell so it's not entirely clear to me what we're missing.

-- annevankesteren.nl

# Sam Tobin-Hochstadt (5 years ago)

On Tue, May 21, 2013 at 2:52 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 8:11 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Ok, that's what I thought was going on. We can make some other function calls which might mutate the Map, just as with any other Map that we get from someone else. In that case, my response to Anne stands -- why is any spec magic needed at all?

Well basically, you want an object that's a Map for all intents and purposes, but does a couple of things differently. So e.g. you need a custom set and all Map operations that use set need to use that new set. Tab's original message explains that however as far as I can tell so it's not entirely clear to me what we're missing.

No, you don't need to do anything differently. Conceptually, there are three things you need:

  1. When the Map is created, before it's handed to the program, some items are added.
  2. Some platform operations also change this map in addition to doing the other things they do.
  3. Some other set of platform operations consult this map when doing their other work.

Obviously, this is the spec perspective; an implementation could have some magic version of the Map that does the update of the internal platform state eagerly when map.set() is called.

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 11:01 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

No, you don't need to do anything differently. Conceptually, there are three things you need:

  1. When the Map is created, before it's handed to the program, some items are added.
  2. Some platform operations also change this map in addition to doing the other things they do.
  3. Some other set of platform operations consult this map when doing their other work.

Obviously, this is the spec perspective; an implementation could have some magic version of the Map that does the update of the internal platform state eagerly when map.set() is called.

How does that ensure that e.g.

map.set("var-" + somethingNotAllowedByCSS, "test")

throws / is ignored (forgot what the desired semantic is)? Or

map.set("var-test", {toString:function(){return"test")}) map.get("var-test")

returns "test"?

-- annevankesteren.nl

# Sam Tobin-Hochstadt (5 years ago)

On Tue, May 21, 2013 at 3:09 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 11:01 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

No, you don't need to do anything differently. Conceptually, there are three things you need:

  1. When the Map is created, before it's handed to the program, some items are added.
  2. Some platform operations also change this map in addition to doing the other things they do.
  3. Some other set of platform operations consult this map when doing their other work.

Obviously, this is the spec perspective; an implementation could have some magic version of the Map that does the update of the internal platform state eagerly when map.set() is called.

How does that ensure that e.g.

map.set("var-" + somethingNotAllowedByCSS, "test")

throws / is ignored (forgot what the desired semantic is)? Or

map.set("var-test", {toString:function(){return"test")}) map.get("var-test")

returns "test"?

If the plan is to do that, then you should not use Map. That isn't a Map.

But why do you want to do this? Why not just have the CSS side ignore properties with bad names/values?

# Brendan Eich (5 years ago)

Sam Tobin-Hochstadt wrote:

If the plan is to do that, then you should_not_ use Map. That isn't a Map.

But that doesn't need to be a proxy, either. It's something with a signature that could be the same as Map's, but with different semantics. I sense people have trouble drawing the line on "is-a". LSP gets invoked but doesn't help. Any better razor, or butterknife?

# David Bruant (5 years ago)

Le 21/05/2013 04:06, Tab Atkins Jr. a écrit :

For the CSS Variables spec I need to define an object with arbitrary string keys, with the initial set determined by the custom properties set in the style rule, and on modification I need to coerce the provided key to a string, and then go mutate the style rule accordingly. When the style rule is mutated to add/remove custom properties, I also need to mutate the exposed keys on the object. (In other words, this object has a bidirectional link with a style rule; it just exposes a more convenient and specialized interface for custom properties specifically.)

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

Right now I'm defining this via the WebIDL getter/setter/etc hooks, which ends up defining an "object map". This is bad practice, though, because anything set on the prototype chain will show up as a (non-own) key, potentially causing confusion.

I'd like to convert this over to an ES Map, as that avoids the above issue and gets me all the Map extras for free, which is nice.

Which exactly? Of all the Map extras, I only see Map.p.clear() and Map.p.size that can't be done as easily with the base object interface (... well... size can be Object.keys().length) Since keys are arbitrary, you'd have to invoke prototype methods through .call. Is it worth it?

However, I don't think it's currently possible to do what I need.

Is it possible add appropriate hooks to the ES spec to let me define the [[MapData]] via a spec, rather than as an initially-empty list of tuples that get/set unconditionally read from? I need to be able to define in spec-ese that the [[MapData]] tuples consist of some list of data from a style rule, and that whenever a value gets set, I first coerce the key to a string, and then go mutate the style rule instead of the [[MapData]] (it then picks up the mutated data by virtue of being defined by the style rule).

(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)

Alternately: Proxies?

yup

David

[1] See "Non-generic built-in functions" under harmony:direct_proxies#wrapping_irregular_objects

# Brendan Eich (5 years ago)

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's). We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

So Tab: why do you want to abuse Map instead of make a custom class?

# David Bruant (5 years ago)

Le 21/05/2013 13:19, Brendan Eich a écrit :

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's).

What is the API exactly? This hasn't been clearly described yet.

We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

Aren't we? That's what I understood when I read (from Tab's initial post): "When the style rule is mutated to add/remove custom properties, I also need to mutate the exposed keys on the object."

In any case, making property appear isn't the only use for proxies. If all property names where known in advance, getter/setters would be fine for the kind of validation/coercion that's expected here. Since they aren't known in advance, a proxy seems suitable.

So Tab: why do you want to abuse Map ...

He wrote in his initial message: "I'd like to convert this over to an ES Map, as that [...] gets me all the Map extras for free, which is nice."

So I guess: what Map extras do you really want/need? Where would the object interface (get/set/"in"/delete/for-of) be insufficient?

...instead of make a custom class?

Sounds like a good idea too at this point.

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 12:19 PM, Brendan Eich <brendan at mozilla.com> wrote:

So Tab: why do you want to abuse Map instead of make a custom class?

As we tried to explain before, the believe is that a lot of Map generics will work directly on this Map-like object, similar to how we want Array methods to work directly on NodeList & co, despite NodeList not being a JS Array (not mutable for one).

-- annevankesteren.nl

# Anne van Kesteren (5 years ago)

On Tue, May 21, 2013 at 12:19 PM, Brendan Eich <brendan at mozilla.com> wrote:

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

I guess I also do not really get this. Sure JavaScript does not have a type system (yet?), but it seems that placing restrictions / coercion on input does not invalidate any of the properties of a map other than that there's a restriction on what goes in the map. To me that seems very much like a subset of a map and all generic functionality written around maps would work on such a map.

-- annevankesteren.nl

# Sam Tobin-Hochstadt (5 years ago)

On Tue, May 21, 2013 at 6:52 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 12:19 PM, Brendan Eich <brendan at mozilla.com> wrote:

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

I guess I also do not really get this. Sure JavaScript does not have a type system (yet?), but it seems that placing restrictions / coercion on input does not invalidate any of the properties of a map other than that there's a restriction on what goes in the map. To me that seems very much like a subset of a map and all generic functionality written around maps would work on such a map.

The following function returns true for all Maps M, Strings k, and JS values v:

function check(M, k, v) {
   M.set(k,v);
   return (v === M.get(k));
}

In fact, this is the essence of what Maps are about. Your proposal doesn't have this property. Therefore, it shouldn't be a Map.

The analogy to NodeList is actually valuable -- NodeLists are pretty different from Arrays, but some of the array generics work on them. If the problem is that the Map functions should be more generic, that's something that could be fixed, but that doesn't mean we should pretend that things are maps when they aren't.

Sam

# Brendan Eich (5 years ago)

Anne van Kesteren wrote:

On Tue, May 21, 2013 at 12:19 PM, Brendan Eich<brendan at mozilla.com> wrote:

So Tab: why do you want to abuse Map instead of make a custom class?

As we tried to explain before, the believe is that a lot of Map generics will work directly on this Map-like object, similar to how we want Array methods to work directly on NodeList& co, despite NodeList not being a JS Array (not mutable for one).

So? Make your custom map-ish thing map-like, just as array-likes work with Array generics: by duck typing.

# Tab Atkins Jr. (5 years ago)

On Tue, May 21, 2013 at 4:19 AM, Brendan Eich <brendan at mozilla.com> wrote:

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's). We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

So Tab: why do you want to abuse Map instead of make a custom class?

Restating from my earlier post:

  1. when someone asks "is this Map-like?" in an appropriately idiomatic JS way, they get a "yes" answer.
  2. when someone adds a new function to Maps in an appropriately idiomatic JS way, the method also applies to this object
  3. when JS expands the set of built-in methods for Map, it also gets applied to this object without me having to update my spec
  4. for all the existing Map methods, I get identical/equivalent methods without having to manually redefine every single one of them

All of these follow from the basic statement that this is a map, because it clearly is. It's limited to string keys and values, but that's just some coercion rules. Every single operation defined on Maps currently is meaningful and useful for this object, and virtually any method you can imagine adding to Maps in the future is also meaningful and useful.

Ducktyping this as a Map is terrible, because it breaks #2/3/4, and arguably #1 depending on what you consider an "appropriately idiomatic" way of asking if something is Map-like.

Just subclassing Maps and providing my own get/set functions is not as terrible, but still bad for a few reasons: I need to study the ES spec very carefully to figure out which operations are "basic" (interact directly with [[MapData]] rather than going through existing operations) and redefine all of them, and anyone who ever does something like Map.prototype.set.call(myObj, ...) will break the invariants I need maintained, with mysterious and undefined results.

This should be super-simple - I just want to supply a custom object as the [[MapData]] which can intercept sets so it can coerce the values beforehand. In every other way, this is a Map.

# Tab Atkins Jr. (5 years ago)

On Tue, May 21, 2013 at 7:19 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

On Tue, May 21, 2013 at 6:52 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, May 21, 2013 at 12:19 PM, Brendan Eich <brendan at mozilla.com> wrote:

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

I guess I also do not really get this. Sure JavaScript does not have a type system (yet?), but it seems that placing restrictions / coercion on input does not invalidate any of the properties of a map other than that there's a restriction on what goes in the map. To me that seems very much like a subset of a map and all generic functionality written around maps would work on such a map.

The following function returns true for all Maps M, Strings k, and JS values v:

function check(M, k, v) { M.set(k,v); return (v === M.get(k)); }

In fact, this is the essence of what Maps are about. Your proposal doesn't have this property. Therefore, it shouldn't be a Map.

Within the type constraints we enforce, this is maintained. That is, if you use string keys and values, it maintains all of the Map invariants. If you use non-string keys or values, we coerce to a string first, so the invariants may not hold in all circumstances. That's the point of coercion.

This argument is like saying that, if "p.foo = obj; p.foo === obj;" isn't maintained, then "p" isn't an Object and should be something else. In reality, we're completely fine with "foo" being a getter or setter with arbitrary effects; in particular, it can apply coercion rules, which is rather common on the web.

If TC39 isn't going to allow us to ever use any of the built-in collection classes just because we have type restrictions we need to enforce, that'll be a pretty raw deal for authors. It'll just mean we continue with our "custom, shitty, incompatible versions of all your standard collections" thing that we've been doing for some time. (And never doubt, for each collection you have, we'll have N slightly incompatible versions, where N is proportional to the number of specs that use something like the collection.)

The analogy to NodeList is actually valuable -- NodeLists are pretty different from Arrays, but some of the array generics work on them. If the problem is that the Map functions should be more generic, that's something that could be fixed, but that doesn't mean we should pretend that things are maps when they aren't.

Making the methods more generic won't help much (though it would probably help slightly) - the problem is that the methods aren't on the objects. Try to guess the relative numbers of people who do "Array.prototype.forEach.call(arrayLike, ...)" versus the numbers who just do a quick "Array.prototype.slice.call(arrayLike)" at the beginning and then rejoice at having a real Array to use.

We need to fix this "type coercion means it's not one of ours" problem, now. It's not good for authors or for the platform as a whole.

# Ron Buckton (5 years ago)

What if the default Map prototype had a configurable but non-writable data property for a @@coerceKey symbol that pointed to a default coercion function. You could subclass Map and provide your own @@coerceKey implementation. Then Map.prototype.set.call() would be forced to run the custom coercion function.

A @@coerceKey override could be used to coerce keys from one type to another, or to provide key validation if you want keys in a specific format. By providing a default implementation that is basically an identity function, you maintain the same current expectations for Map. This then gives developers the ability to further customize the behavior of Map in subclasses, giving the class more flexibility.

If that were the case, there could also be a @@coerceValue symbol on Map.prototype as well as Set.prototype. Possibly even Array.prototype as well for Array subclasses, as a means of limiting array contents to a specific type. The coercion functions could also be used to allow a subclass to Mark itself as read-only and throw on attempts at modification. Default implementations could verify whether the coercion function has changed from the default and skip the coercion calls as a performance optimization.

# Tab Atkins Jr. (5 years ago)

On Tue, May 21, 2013 at 9:20 PM, Ron Buckton <rbuckton at chronicles.org> wrote:

What if the default Map prototype had a configurable but non-writable data property for a @@coerceKey symbol that pointed to a default coercion function. You could subclass Map and provide your own @@coerceKey implementation. Then Map.prototype.set.call() would be forced to run the custom coercion function.

A @@coerceKey override could be used to coerce keys from one type to another, or to provide key validation if you want keys in a specific format. By providing a default implementation that is basically an identity function, you maintain the same current expectations for Map. This then gives developers the ability to further customize the behavior of Map in subclasses, giving the class more flexibility.

If that were the case, there could also be a @@coerceValue symbol on Map.prototype as well as Set.prototype. Possibly even Array.prototype as well for Array subclasses, as a means of limiting array contents to a specific type. The coercion functions could also be used to allow a subclass to Mark itself as read-only and throw on attempts at modification. Default implementations could verify whether the coercion function has changed from the default and skip the coercion calls as a performance optimization.

These aren't bad ideas, and they get me most of the way to where I need to be. I still need some way to detect when an entry is added/modified/deleted, so I can update the style rule it's associated with.

# Allen Wirfs-Brock (5 years ago)

On May 21, 2013, at 6:42 PM, Tab Atkins Jr. wrote:

On Tue, May 21, 2013 at 4:19 AM, Brendan Eich <brendan at mozilla.com> wrote:

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's). We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

So Tab: why do you want to abuse Map instead of make a custom class?

Restating from my earlier post:

  1. when someone asks "is this Map-like?" in an appropriately idiomatic JS way, they get a "yes" answer.

First you have to define what "Map-like" means. Is it supporting get/set/has? Is it any object that conforms to Sam's "check" invariant?

There is currently no specified way to ask if something is "Map-like" because we don't know. BTW, in the school of dynamic language OO development that I come from it is considered an "anti-pattern" to ask if something is "a Foo" or is "Foo-like". If your (informal) contract says that you require that somebody pass you a Foo-like object, just assume that they did. If they don't some sort of error is likely to occur soon enough. You have to bake sure that you actively mantain essential internal invariants but there is no real need to sweat about higher-level "type check" around the edges.

In the ES6 built-ins where there is a need to do a quick behavoral check for a "kind" of object we defined a method such as @@isRegExp which is used to enable various string methods to recognize multiple different concrete implementations of "RegExp-like" objects. You could imagine us providing [email protected]@isMap but we currently have no strong use cases for it.

  1. when someone adds a new function to Maps in an appropriately idiomatic JS way, the method also applies to this object

You mean add methods to Map.prototype? Modify other people's (including built-in) prototypes is rather frowned upon. Map is designed to be subclassed. If you what a new kind of map with additional methods the appropriate way to do that would be to add those methods of a subclass and use instances of the subclass instead of Map.

  1. when JS expands the set of built-in methods for Map, it also gets applied to this object without me having to update my spec.

Why do you think this will happen? First let mean define what "generic map method" would mean to mean. It is a method that does not directly depend upon the internal [[MapData]] state. It only accesses that state indirectly via get/set/has etc.

Currently there are no generic Map.prototype methods. They all are directly tied to the [[MapData]] representation for some specific reason.

At this point in time it isn't obvious what generic map methods would be useful to add in the future.

  1. for all the existing Map methods, I get identical/equivalent methods without having to manually redefine every single one of them

Except that all of the existing methods are dependent upon the specific [[MapData]] representation. If you want to use a different representation of the data for a new kind of map you are going to have to over-ride all of the existing methods anyway.

You could define Map using a two level implementation strategy where, for example, the "get" method called perhaps an @@get method and where all representational knowledge would be in the @@ methods. However, you you still have to over-ride the same number of @@ methods so I don't see a major gain in that approach.

All of these follow from the basic statement that this is a map, because it clearly is. It's limited to string keys and values, but that's just some coercion rules. Every single operation defined on Maps currently is meaningful and useful for this object, and virtually any method you can imagine adding to Maps in the future is also meaningful and useful.

Ducktyping this as a Map is terrible, because it breaks #2/3/4, and arguably #1 depending on what you consider an "appropriately idiomatic" way of asking if something is Map-like.

Just subclassing Maps and providing my own get/set functions is not as terrible, but still bad for a few reasons: I need to study the ES spec very carefully to figure out which operations are "basic" (interact directly with [[MapData]] rather than going through existing operations) and redefine all of them, and anyone who ever does something like Map.prototype.set.call(myObj, ...) will break the invariants I need maintained, with mysterious and undefined results.

There are none. All the current Map methods are intentionally dependent upon [[MapData]]

This should be super-simple - I just want to supply a custom object as the [[MapData]] which can intercept sets so it can coerce the values beforehand. In every other way, this is a Map.

Note [[MapData]] isn't just a specification abstraction over a data store. It also abstracts all the access patterns of that data store.

# Allen Wirfs-Brock (5 years ago)

On May 22, 2013, at 5:20 AM, Ron Buckton wrote:

What if the default Map prototype had a configurable but non-writable data property for a @@coerceKey symbol that pointed to a default coercion function. You could subclass Map and provide your own @@coerceKey implementation. Then Map.prototype.set.call() would be forced to run the custom coercion function.

A @@coerceKey override could be used to coerce keys from one type to another, or to provide key validation if you want keys in a specific format. By providing a default implementation that is basically an identity function, you maintain the same current expectations for Map. This then gives developers the ability to further customize the behavior of Map in subclasses, giving the class more flexibility.

It's an old Smalltalk trick to use subclass refinement hooks like this. Whether they make sense or not is largely dependent upon how common the use case is for that dimension of variability in subclasses. Having them adds both complexity and probably a bit of a performance hit. It is probably a reasonable design choice if the hook is frequently needed.

I don't think we have enough data to say whether this will be the case for Map. My inclination would be to leave things as they are for now and see how actual usage evolves. These sorts of hooks are backwards compatible extensions that can always be added in the future.

# Tom Van Cutsem (5 years ago)

2013/5/21 Brendan Eich <brendan at mozilla.com>

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's). We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

My 2c: I agree, don't express this as a proxy. You want a simple key-value mapping. Expressing that as a proxy drags in lots of unnecessary aspects of JS's object model (e.g. prototypes, property attributes, etc.) You don't want/need to do that for this abstraction.

# Brendan Eich (5 years ago)

Tab Atkins Jr. wrote:

On Tue, May 21, 2013 at 4:19 AM, Brendan Eich<brendan at mozilla.com> wrote:

So Tab: why do you want to abuse Map instead of make a custom class?

Restating from my earlier post:

  1. when someone asks "is this Map-like?" in an appropriately idiomatic JS way, they get a "yes" answer.
  2. when someone adds a new function to Maps in an appropriately idiomatic JS way, the method also applies to this object
  3. when JS expands the set of built-in methods for Map, it also gets applied to this object without me having to update my spec
  4. for all the existing Map methods, I get identical/equivalent methods without having to manually redefine every single one of them

There aren't many Map methods. Write your own workalikes if you need 'em. Done.

All of these follow from the basic statement that this is a map, because it clearly is.

Circular arguments won't help here. A Map doesn't have throw-on-certain-key behavior; a Map doesn't act at a distance on other data without metaprogramming.

You've falling into a nominal typing trap. JS doesn't have a Map that everyone must bend or break to suit quasi-Map-like use-cases, any more than Array must be directly bent (or hooked with @@coerceKey trash) to be a NodeList.

Make a different data structure (it can subclass if you insist; it could instead use Map internally).

# Brendan Eich (5 years ago)

Tab Atkins Jr. wrote:

If TC39 isn't going to allow us to ever useany of the built-in collection classes just because we have type restrictions we need to enforce, that'll be a pretty raw deal for authors.

Whining about TC39 like this is bad for business. Should I whine about W3C to even up the bad karma?

You can do whatever you want with a subclass or a wrapper. Your o.p. had a parenthetical aside about subclass hardships:

(One way to do thistoday 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.)

----- end cite -----

But there's no free lunch. Either Map needs hooks lumbering its spec and implementations for all users, or you need to do the hooking. Without evidence that others need the hooking, as Allen argues, the burden's on you. Buy your own lunch.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 3:40 AM, Brendan Eich <brendan at mozilla.com> wrote:

There aren't many Map methods. Write your own workalikes if you need 'em. Done.

Really? You're seriously saying that I, and every other spec author, every time we need to spec something that's just like a built-in collection class but has a type restriction or some other little tweak, should just reinvent Map/Set/whatever?

I refuse to believe you're actually saying that, because of how bad it is for authors. This is a textbook example of elevating theoretical purity over spec authors and web authors.

Does this mean that every time an author needs a Map, but only needs string keys, they should reinvent Map as well? Or is it just because the platform actually needs to enforce the restriction, and authors are allowed to

All of these follow from the basic statement that this is a map, because it clearly is.

Circular arguments won't help here. A Map doesn't have throw-on-certain-key behavior; a Map doesn't act at a distance on other data without metaprogramming.

You've falling into a nominal typing trap. JS doesn't have a Map that everyone must bend or break to suit quasi-Map-like use-cases, any more than Array must be directly bent (or hooked with @@coerceKey trash) to be a NodeList.

Make a different data structure (it can subclass if you insist; it could instead use Map internally).

It's a map (that is, a weakly ordered store of key/value pairs). It happens to have a type restriction, but as long as you stay within that type, every Map invariant you can think of is maintained exactly (and we wouldn't throw, we'd toString). It also happens to have a linkage with another data structure, but that other structure is under the authors' complete control, so in practice it just means that there are two get/set/etc methods with slightly different signatures.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 3:44 AM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If TC39 isn't going to allow us to ever useany of the built-in

collection classes just because we have type restrictions we need to enforce, that'll be a pretty raw deal for authors.

Whining about TC39 like this is bad for business. Should I whine about W3C to even up the bad karma?

Please, please, let's drop the silly turf war rhetoric, it's embarrassing. The spec that I'm writing this for happens to be produced under the W3C aegis, but that's completely irrelevant to my work. I named TC39 because that's the specific group I'm talking to right now, and which is pushing back, not because I think it matters one whit where a spec is produced.

You can do whatever you want with a subclass or a wrapper. Your o.p. had a parenthetical aside about subclass hardships:

(One way to do thistoday 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.)

----- end cite -----

But there's no free lunch. Either Map needs hooks lumbering its spec and implementations for all users, or you need to do the hooking. Without evidence that others need the hooking, as Allen argues, the burden's on you. Buy your own lunch.

The evidence for needing hooking is all around. We need maps and sets and array all the time in DOM and CSSOM and other specs, it's just that we usually lumber around and invent crappy versions of them because they didn't exist in JS before. Look at dataset. Look at CSSStyleRule. Look at my example. I could just iterate through specs and find examples all day where we've done something dumb because of the lack of a good, standardized data structure, but I don't think you actually need me to do that; you're familiar enough with the mistakes the web platform has made.

# Brendan Eich (5 years ago)

On May 22, 2013, at 4:58 PM, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:

On Wed, May 22, 2013 at 3:40 AM, Brendan Eich <brendan at mozilla.com> wrote:

There aren't many Map methods. Write your own workalikes if you need 'em. Done.

Really? You're seriously saying that I, and every other spec author, every time we need to spec something that's just like a built-in collection class but has a type restriction or some other little tweak, should just reinvent Map/Set/whatever?

Subclass or wrap, you read that already.

Look, you don't add hooks to Array.prototype for NodeList. Same goes for Map. If subclassing gets you some goodies without losing others, ok.

I refuse to believe you're actually saying that, because of how bad it is for authors. This is a textbook example of elevating theoretical purity over spec authors and web authors.

Rubbish. Put your hooks in your own overrides. Pay for your own lunch.

# Brendan Eich (5 years ago)

On May 22, 2013, at 5:10 PM, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:

On Wed, May 22, 2013 at 3:44 AM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If TC39 isn't going to allow us to ever useany of the built-in

collection classes just because we have type restrictions we need to enforce, that'll be a pretty raw deal for authors.

Whining about TC39 like this is bad for business. Should I whine about W3C to even up the bad karma?

Please, please, let's drop the silly turf war rhetoric, it's embarrassing.

My point, your move. I did not bring it up, and I wrote the above rhetorical question solely to get you to cut it out.

But there's no free lunch. Either Map needs hooks lumbering its spec and implementations for all users, or you need to do the hooking. Without evidence that others need the hooking, as Allen argues, the burden's on you. Buy your own lunch.

The evidence for needing hooking is all around. We need maps and sets and array all the time in DOM and CSSOM and other specs, it's just that we usually lumber around and invent crappy versions of them because they didn't exist in JS before. Look at dataset. Look at CSSStyleRule. Look at my example. I could just iterate through specs and find examples all day where we've done something dumb because of the lack of a good, standardized data structure, but I don't think you actually need me to do that; you're familiar enough with the mistakes the web platform has made.

NodeList extends Array now but is not literally Array plus hooks. Same for Map and the quite different thing you're specifying.

OOP has not been replaced by HookOP.

Tom Van Cutsem's reply is on target: do not add unstratified traps to common base classes. Do use compositional and general-purpose extension mechanisms in ES6, including classes.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 9:44 AM, Brendan Eich <brendan at mozilla.com> wrote:

NodeList extends Array now but is not literally Array plus hooks. Same for Map and the quite different thing you're specifying.

NodeList is an interesting case, actually. It's an Array, but with a type restriction. Live NodeLists even have basically the same linkage behavior I'm talking about.

What do I need to do to get a Map like that?

# Domenic Denicola (5 years ago)

From: Tab Atkins Jr. [jackalmage at gmail.com]

NodeList is an interesting case, actually. It's an Array, but with a type restriction.

What do you mean by that? Surely you don't mean "can only store nodes":

> var nodeList = document.querySelectorAll("div");

undefined

> nodeList.length

22

> nodeList[22] = "not a node";
"not a node"

> nodeList[22]
"not a node"

On the other hand, not sure what's going on here:

> nodeList[1] = "not a node either";
"not a node either"

> nodeList[1]
<div class=​"banner">​…​</div>​

> Object.getOwnPropertyDescriptor(nodeList, "1")

Object {value: div.banner, writable: true, enumerable: true, configurable: true}

(both results in Chrome 27. Firefox 21 has a different pathology... It returns undefined for nodeList[22], although Object.isExtensible(nodeList) === true. But it gets the second case right, giving writable: false to correctly reflect the behavior there.)

# David Bruant (5 years ago)

Le 22/05/2013 19:02, Tab Atkins Jr. a écrit :

On Wed, May 22, 2013 at 9:44 AM, Brendan Eich <brendan at mozilla.com> wrote:

NodeList extends Array now but is not literally Array plus hooks. Same for Map and the quite different thing you're specifying. NodeList is an interesting case, actually. It's an Array, but with a type restriction.

I'm a bit lost here. What exactly do you label as "Array"? What is a "type restriction"?

For sure, Array.isArray(anyNodeList) === false So the ES spec seems to disagree with the statement "a NodeList is an Array"

Live NodeLists even have basically the same linkage behavior I'm talking about.

What do I need to do to get a Map like that?

Proxy is the answer to your question (FWIW I've made a proxy-based implementation of arrays a while back [1]). But both cases can't really be compared. From a dev perspective, the interface of an array is its own properties (numerical up to 2^32-1, connection with the .length property...) and the interface of a map is the methods/accessors on the prototype ([[MapData]] being "just" a spec tool that could be replaced by anything else as long as prototype methods were behaving observably the same)

If your object can have any property name, that can shadow the get/set/has property you need to interact with your map. I'm not sure what the solution is, but a proxy doesn't sound like a good idea at least because of the shadowing issue.

David

[1] DavidBruant/HarmonyProxyLab/blob/master/ProxyArray/ProxyArray.js

# Andrea Giammarchi (5 years ago)

is simply static collection, so immutable:

var all = document.querySelectorAll("*");
var length = all.length;
all[length] = 'whatever';
alert(all.length === length);
[].push.call(all, 'whatever');
alert(all.length === length);

you can put properties because is extensible but you won't change the index nature of that collection neither you can change addressed variables ...

NodeList instances are created like this:

function Ecw(value) {
  return {
    enumerable: true,
    writable: false,
    configurable: false,
    value: value
  };
}

Object.defineProperties(
  Object.create(NodeList.prototype),
  {
    "0": Ecw(document.documentElement),
    "1": Ecw(document.head),
    "2": Ecw(document.body),
    "length": Ecw(3)
  }
);

this one regardless the returned descriptor info (at least in Chrome) which is not trustable with hosted natives

# Andrea Giammarchi (5 years ago)

I believe Tab is asking for something like:

var MyNodeList = new ArrayType(EntryType, length);

where EntryType is his own type and the result is an array like collection or map.

At least, for what I understood, looks like he's asking for JS CTypes, something rumored a while ago but never heard about again (while asm.js has been introduced/implemented already)

If I am mistaking, apologies.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 10:35 AM, David Bruant <bruant.d at gmail.com> wrote:

Le 22/05/2013 19:02, Tab Atkins Jr. a écrit :

On Wed, May 22, 2013 at 9:44 AM, Brendan Eich <brendan at mozilla.com> wrote:

NodeList extends Array now but is not literally Array plus hooks. Same for Map and the quite different thing you're specifying.

NodeList is an interesting case, actually. It's an Array, but with a type restriction.

I'm a bit lost here. What exactly do you label as "Array"? What is a "type restriction"?

For sure, Array.isArray(anyNodeList) === false So the ES spec seems to disagree with the statement "a NodeList is an Array"

Live NodeLists even have basically the same linkage behavior I'm talking about.

What do I need to do to get a Map like that?

Proxy is the answer to your question (FWIW I've made a proxy-based implementation of arrays a while back [1]). But both cases can't really be compared. From a dev perspective, the interface of an array is its own properties (numerical up to 2^32-1, connection with the .length property...) and the interface of a map is the methods/accessors on the prototype ([[MapData]] being "just" a spec tool that could be replaced by anything else as long as prototype methods were behaving observably the same)

I don't agree; if that were the case, the return value of querySelectorAll() would be adequate for my uses.

The interface of an array is also the Array methods. Those are the valuable things to copy, and it's why every time I write a non-trivial webpage, I include at the top:

function findAll(sel) { return [].slice.call(document.querySelectAll(sel)); }

Array are damned useful. Things that do a crappy duck-typing of Arrays by just exposing indexed properties and a .length property... not so much.

If your object can have any property name, that can shadow the get/set/has property you need to interact with your map. I'm not sure what the solution is, but a proxy doesn't sound like a good idea at least because of the shadowing issue.

I think you may misunderstand my OP. The current interface I have defined in my spec is an arbitrary getter/setter/etc that can take any property name (an "object map"), because that's easy to write and manage in WebIDL. I'd like to move to using a Map, because it's more useful for authors, and it avoids all the issues with shadowing or properties set on the prototype.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 11:00 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:

I believe Tab is asking for something like:

var MyNodeList = new ArrayType(EntryType, length);

where EntryType is his own type and the result is an array like collection or map.

At least, for what I understood, looks like he's asking for JS CTypes, something rumored a while ago but never heard about again (while asm.js has been introduced/implemented already)

If I am mistaking, apologies.

I have no idea what you're claiming I'm asking for. My request is in my OP - I have a CSSOM API that currently uses an "object map", that I'd like to swap out for a real Map. Because of the way CSS works, the keys and values are both coerced into strings, but that's it.

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 1:48 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On May 21, 2013, at 6:42 PM, Tab Atkins Jr. wrote:

On Tue, May 21, 2013 at 4:19 AM, Brendan Eich <brendan at mozilla.com> wrote:

David Bruant wrote:

This description ("arbitrary string keys", "bidirectional link with style rule") suggests that you want a proxy (canonical values are in the style rule, the proxy is just a façade with a bit of validation/coercion logic). That's the sort of use case they've been introduced for.

I don't see why a proxy is required if the API is get/set/has (like Map's). We're not making properties appear without knowing their names, so this is not a proxy use-case on its face.

Of course, coercing key type makes the API not Map. So if the bi-directionality is important, this would be a custom Map-like class.

So Tab: why do you want to abuse Map instead of make a custom class?

Restating from my earlier post:

  1. when someone asks "is this Map-like?" in an appropriately idiomatic JS way, they get a "yes" answer.

First you have to define what "Map-like" means. Is it supporting get/set/has? Is it any object that conforms to Sam's "check" invariant?

A store of key/value pairs. That's the definition of a map.

There is currently no specified way to ask if something is "Map-like" because we don't know. BTW, in the school of dynamic language OO development that I come from it is considered an "anti-pattern" to ask if something is "a Foo" or is "Foo-like". If your (informal) contract says that you require that somebody pass you a Foo-like object, just assume that they did. If they don't some sort of error is likely to occur soon enough. You have to bake sure that you actively mantain essential internal invariants but there is no real need to sweat about higher-level "type check" around the edges.

I know that prototype checking isn't a great way to check if you satisfy some contract. On the other hand, ES has no other way to do so, and especially no other way to say "this method should work for everything that satisfies the 'map' contract".

  1. when someone adds a new function to Maps in an appropriately idiomatic JS way, the method also applies to this object

You mean add methods to Map.prototype? Modify other people's (including built-in) prototypes is rather frowned upon. Map is designed to be subclassed. If you what a new kind of map with additional methods the appropriate way to do that would be to add those methods of a subclass and use instances of the subclass instead of Map.

Frowned upon, but super useful and widely done. Say I wanted to add a 'reduce' method to Map (same as Array#reduce, but passes in the key as an argument too). There's nothing wrong with applying this to every Map, so I'd add it to Map.prototype. Subtyping Map means that I don't get this for free; I have to pass every outside Map through a converter function, for no real reason except theoretical purity.

  1. when JS expands the set of built-in methods for Map, it also gets applied to this object without me having to update my spec.

Why do you think this will happen?

Do you really think tc39 will never define more Map methods? More Set methods? Given the history of every other built-in class, it seems certain that we'll gradually expand the surface of Map and Set.

  1. for all the existing Map methods, I get identical/equivalent methods without having to manually redefine every single one of them

Except that all of the existing methods are dependent upon the specific [[MapData]] representation. If you want to use a different representation of the data for a new kind of map you are going to have to over-ride all of the existing methods anyway.

You could define Map using a two level implementation strategy where, for example, the "get" method called perhaps an @@get method and where all representational knowledge would be in the @@ methods. However, you you still have to over-ride the same number of @@ methods so I don't see a major gain in that approach.

[[MapData]] is just a list of tuples. All I need for my spec (and for future specs that want something Map-like) is the ability to define my own list of tuples (linked to another object), and the ability to intercept sets/deletes so I can do coercion and adjustment of other objects.

Unless I'm completely missing something, there are at most 4 "basic" things to override, the same 4 things that you override to make an "object map": create/read/update/delete.

Is it really more complicated than this? If so, how?

# Andrea Giammarchi (5 years ago)

you wrote: What do I need to do to get a Map like that?

I answered but I've mistaken your original question and apologized already.

# Allen Wirfs-Brock (5 years ago)

On May 22, 2013, at 7:21 PM, Tab Atkins Jr. wrote:

-ride the same number of @@ methods so I don't see a major gain in that approach.

[[MapData]] is just a list of tuples. All I need for my spec (and for future specs that want something Map-like) is the ability to define my own list of tuples (linked to another object), and the ability to intercept sets/deletes so I can do coercion and adjustment of other objects.

[[MapData]] is a specification abstraction for an implementation chosen data structure. The spec. only describes the semantics that it must be able to represent. It is not necessarily (and hopefully isn't) implemented as a list of tuples. Regardless, it is only directly accessible at the implementation level of a ES engine. Neither you or anybody else can directly interact with it. However, you can indirectly interact with [[MapData]] through the built-in [[MapData]] methods.

Unless I'm completely missing something, there are at most 4 "basic" things to override, the same 4 things that you override to make an "object map": create/read/update/delete.

Is it really more complicated than this? If so, how?

as complicated as this:

class StringMap extends Map { delete(key) {return super(String(key)); get(key) {return super(String(key)); has(key) {return super(String(key)); set(key, value) {return super(String(key), value)); }

# Tab Atkins Jr. (5 years ago)

On Wed, May 22, 2013 at 3:43 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On May 22, 2013, at 7:21 PM, Tab Atkins Jr. wrote:

-ride the same number of @@ methods so I don't see a major gain in that approach. [[MapData]] is just a list of tuples. All I need for my spec (and for future specs that want something Map-like) is the ability to define my own list of tuples (linked to another object), and the ability to intercept sets/deletes so I can do coercion and adjustment of other objects.

[[MapData]] is a specification abstraction for an implementation chosen data structure. The spec. only describes the semantics that it must be able to represent. It is not necessarily (and hopefully isn't) implemented as a list of tuples. Regardless, it is only directly accessible at the implementation level of a ES engine. Neither you or anybody else can directly interact with it. However, you can indirectly interact with [[MapData]] through the built-in [[MapData]] methods.

Right; of course the implementation won't be a list of tuples, but the spec description is.

Unless I'm completely missing something, there are at most 4 "basic" things to override, the same 4 things that you override to make an "object map": create/read/update/delete.

Is it really more complicated than this? If so, how?

as complicated as this:

class StringMap extends Map { delete(key) {return super(String(key)); get(key) {return super(String(key)); has(key) {return super(String(key)); set(key, value) {return super(String(key), value)); }

That works great until someone does "Map.prototype.set.call(strmap, obj1, obj2)", at which point the map is corrupted. You cannot guarantee invariants via simple subclassing. You have to hook the four "underlying" operations, which all the rest are defined on top of.

# Tab Atkins Jr. (4 years ago)

Restarting this thread, because it's still an issue a year+ on.

In the Font Loading spec, I have a FontFaceSet interface, which is a Set that contains FontFace objects, plus a few other methods and attributes.

Except that it's not a Set, because I can't subclass Sets reliably. Instead, I have to duplicate the entire Set interface as it exists today, and have it delegate to a hidden internal Set object. This means that when TC39 extends the Set interface, FontFaceSet will have to be manually updated and bugfixed to contain its own version of the new methods. It also means that when authors define new methods on Set.prototype, they don't apply to FontFaceSet objects unless they manually copy them to FontFaceSet.prototype as well.

This is obviously bad. But I can't just directly subclass Set. If I do, then authors can do Set.prototype.add(ffset, "hahaha, I'm a string, not a font face!"), and then the Set is corrupted. I could, of course, defensively write all the FFS operations to check each entry for being a FontFace object first, but that's not quite enough, because authors also have to do this when iterating a FFS.

It's accepted and idiomatic for all sorts of methods in the web platform to do typechecks on their arguments, and throw errors when they don't match what's expected. It's impossible for me to robustly do this for the Set and Map methods, though. Can we come up with something that allows me to enforce these kinds of checks, like I'm currently doing with my Set-lookalike, and like we do everywhere else in the entire web platform?

# Domenic Denicola (4 years ago)

When I last looked at this, it seemed like a perfect use case for Traits. A "Set-like" trait would require definitions for add, delete, and [Symbol.iterator], but provide has, forEach, entries, keys, values, clear, and size, plus any future interfaces.

Then you would implement such set-likes by mixing in the set-like trait into your prototype, which contains type-restricted definitions for add, delete, and [Symbol.iterator] (plus probably some of the other methods for performance reasons).

Presumably a future version of ES that contains traits would define set-like, map-like, and other traits that could be used.

# Tab Atkins Jr. (4 years ago)

Yes, something Trait-like would be sufficient.

# Ron Buckton (4 years ago)

Sorry for the short reply here, as I'm not at my PC.

Would having an @@isValue (Map) and @@isKey (Set/Map) work?

Set.prototype[@@isValue] = value => true; // default

Built -in Set operations would check values against @@isValue and throw. You can then override it in your subclass and make it non-configurable.

Alternatively, there could be an options argument on the Set constructor where you could supply a value filter.

# Mark S. Miller (4 years ago)

On Mon, Aug 11, 2014 at 6:33 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

Presumably a future version of ES that contains traits would define set-like, map-like, and other traits that could be used.

Hopefully. But any future JS traits system should somehow play well with JS classes. The semantics of classes we accepted into ES6 makes that difficult but probably not impossible.

Just for the record: Some of the rejected class proposals were designed to play well with future traits systems. In the end I agree with the pragmatics that caused these to be rejected in favor of the current one. But we paid the costs of making traits harder and more awkward, to the point that we may never have one we deem acceptable. We'll see.

# Ron Buckton (4 years ago)

That should have been @@isValue (Set/Map) and @@isKey (Map.

The downside of having an @@isValue filter is that there are likely still ways to get around it (subclasses could define their own, create a new object whose [[Prototype]] is an instance of your subclass with its own @@isValue, etc.). Too bad there's no way to really seal a member.

The options argument could be a definitive solution, though. The Set constructor could use the provided options to set an internal slot for a value filter. There's possibly still a hole that can't be closed due to constructor/prototype tricks.

# Allen Wirfs-Brock (4 years ago)

On Aug 11, 2014, at 5:55 PM, Tab Atkins Jr. wrote:

It's accepted and idiomatic for all sorts of methods in the web platform to do typechecks on their arguments, and throw errors when they don't match what's expected. It's impossible for me to robustly do this for the Set and Map methods, though. Can we come up with something that allows me to enforce these kinds of checks, like I'm currently doing with my Set-lookalike, and like we do everywhere else in the entire web platform?

You can imaging the specification of Set being enhanced to include a private slot whose value was a function that was used to validate values that were being added to the set. Such a function could be (optionally) set when a set is created and would be called by the built-in add method. Somebody might want to develop a proposal for ES7. I think it could be done in a manner that was backwards compatible with the E#S6 Set spec. You could also do something similar with Map and a function that validates both keys and values.