typeof extensibility, building on my Value Objects slides from Thursday's TC39 meeting

Tab Atkins Jr. jackalmage at gmail.com
Fri Aug 2 12:59:55 PDT 2013


On Fri, Aug 2, 2013 at 12:06 PM, Allen Wirfs-Brock
<allen at wirfs-brock.com> wrote:
> On Aug 1, 2013, at 5:36 PM, Tab Atkins Jr. wrote:
>> On Thu, Aug 1, 2013 at 5:29 PM, Brendan Eich <brendan at mozilla.com> wrote:
>>> Tab is still looking for a MapLite (tm) that can be customized with hooks.
>>> Obviously to avoid infinite regress, the MapLite bottoms out as native, and
>>> the hooks are on the outside (in Map). Tab, do I have this right?
>>
>> Right.  Map has a medium-sized interface, with several methods that
>> don't provide new capabiltiies, but only convenience/performance.  As
>> well, I expect the interface to grow over time.
>>
>> Making a Map subclass that needs to audit its data on input or output
>> is thus difficult, because when a new method gets added to Map, either
>> it's able to pierce through your interventions and get at the raw data
>> directly, or it simple doesn't work until you update.  Even for the
>> existing methods, you have to manually override the conveniences; just
>> overriding delete() just protect you against a clear().
>>
>> MapLite has the smallest possible useful semantics for a Map
>> (basically, the same semantics that the spec uses in terms of
>> meta-operations), and thus is useful as an interception target.  You
>> can then just subclass Map and only override the MapLite at its core,
>> secure in the knowledge that new methods will Just Work (tm) in terms
>> of the provided bedrock operations on the MapLite.
>
> I'm all for extensible abstractions based upon abstract algorithms that decompose into a set of over-ridable primitives.  That that is pretty much what the current ES6 specification of Map provides.  I assume that we agree that at the core of map there needs to be an encapsulated implementation of a key/value store.  The question Tab is asking essentially is whether the methods of Map are too tightly couple to one specific key/value store implementation.  Or viewed from another perspective, would ES6 be more flexible if the public abstraction of Map and the default key/value store (Map-lite) were separated into distinct classes.
>
> Let's look at the Map methods to see if it is possible and worthwhile to make it anymore extensible then it already is and whether it makes sense to move some of the functionality into a Map-lite.
>
> 'get'/'set'/'delete': The fundamental operations of Map are 'get', 'set', and 'delete'.  If you look at the specification of these methods, you will see that they are almost pure primitives dealing with the mechanism of the encapsulated key/value store.  If we separated the default key/value store into a separate Map-lite object it would have the equivalent of these methods and 'get'/'set''/delete' on the  more generic Map would simply delegate to them.  There is very little in the way of policy that could be separated from the default implementation. The major policy decisions within 'get'/'set'/'delete' are 'set' handling of duplicate elements, and 'get'/'delete' handling of missing elements.  Map's 'set' over-writes the value of an already existing element, 'get' returns undefined as the value for missing keys, and 'delete' returns false for a missing key.  A Map-lite store would still have to have some sort of defaults for these cases and these are good defaults for JS. If you want to change those defaults it is easy to encapsulate or subclass a Map and over-ride these policies with get/set/delete wrappers.
>
> ''has': is also an almost pure operation upon the primitive key/value store.  It would be possible for a Map-lite to expose a primitive method that combines the capabilities of 'get'/'has' such that Map might define its distinct 'get' and 'has'  policies in terms of that combined operation.  However the combined operation would require  more complex arguments and/or result design and additional logic in the Map-level methods.  I think it is more pragmatic to use a default primitive key/value store that directly exposes the default policies and push the complexity of  alternative policies into any new abstractions that wants them.

Yes, I agree that 'has' is useful as a primitive alongside
get/set/delete.  At the very least, it means that you don't need a
sentinel value to distinguish a missing key from a key set to the
default value.

> 'size' is essentially a primitive of the key/value store.

It can be defined in terms of iterations, but I'm fine with this being
primitive because it's such a small data leak.  If you're subclassing
Map such that the naive .size isn't what you want, you're doing
something rather unusual.

> 'clear' could be expressed in terms of key iteration and the 'delete' methods. We haven't talked about iteration yet and whether it needs to be part of a basic key/value store API.  But we can skip ahead because 'clear' is really about pragmatics.  It's a method that is useful when a client knows it wants to delete all elements from a key/value store.  It primarily exists as a channel to convey that intent to lower level implementation layers, based upon the assumption that a bulk delete can probably be performed more efficiently than a bunch of individual deletes.  So, even if you have a basic key/value store you will want to have a 'clear' method on it to support such bulk delete use cases.

I'm less convinced of 'clear' - it's clearly a product of iteration +
'delete'.  You're right that it's often more efficient to do directly,
though.

> So ES6 Map with 'get'/'set'/'delete'/'has'/'size'/'clear' is essentially a "Map-lite".  What about the rest of Map's methods?
>
> They are all about iteration. There is the 'forEach' method that directly iterates over the keys/values of a Map and 'keys'/'values'/'entries'/@@iterator all of which are used to obtain an iterator over the contents of a map.
>
> So should iteration be part of a Map-lite and should an iteration ordering be mandated. Regard ordering, JS experience shows us that developers generally expect iteration order to be consistent across all implementation.  So a Map-lite that supports iteration without a specified order would be an interoperability hazard. If Map-lite had no iteration methods we could still implement forEach and the iterator factories if Map maintain a separate ordering data structure that it updated on every 'set'/'delete' operation.  But such a side table is likely to be far less efficient than any means of element ordering that are directly implemented as part of the key/value store. So, so for ES, it makes sense for something like 'forEach' to be part of a Map-lite.
>
> The iterator factory methods of map are all just short-cuts for creating "MapIterator" objects which are current specified as "friends" (in the C++ sense) that know about internal key/value store encapsulated by Map objects.  The current spec. for MapInterator was written before we added 'forEach' to Map and arguably it could be re-specified in terms of 'foreach'.  However, that may take away some implementation level flexibility.  Regardless, it's a possibility that I think I'll look into more deeply.
>
> Where we end up with is that the operations we would want for a Map-lite are get/set/delete/has/size/clear/forEach. They all need to know the implementation details of the underlying key/value store and for pragmatic reasons it doesn't make sense to try to reduce them to a smaller set of primitives.

Yes.  You need exactly one of the iteration methods as a primitive.
Today's world, where you have to override all four of them if you want
to override any of them, feels very similar to forcing someone to
override all six boolean comparisons rather than just defining them
all in terms of less-than and equality.  (Like Brendan, I prefer the
default to be one of the external iterators, either keys() or
entries().)

> That is exactly the interface of ES6 Map except that Map adds the trivial iterator factory method.  It simply isn't worth having the added complexity of distinct Map and Map-lite classes simply to not have the iterator factory methods. For ES6 Map is Map-lite

Except for the iterators and maybe 'clear', yes.

> We can debate whether future new methods should be added to Map or be made part of  distinct new classes.  I'd suggest the latter for most cases.  New methods are likely to incorporate domain concepts (eg, DOM stuff) that are not an essential part of the basic key/value store abstraction.  It's better to define an appropriate domain specific abstraction that encapsulates a Map (or if you're an old school OO'er subclasses it) than it is to start adding new methods.

I find it quite likely that we'll add more Map methods in the future.
For example, .update() from Python's dict is easy to write yourself,
but it's useful *often enough* that it's likely worthwhile to add.
Same with .pop(), .popitem(), and setdefault().  It bothers me that if
an author monkey-patches these onto Map.prototype themselves, they'll
automatically work on my subclass (because they're defined in terms of
the primitives that I override), but if we then add it to the
language, it'll break (because it's defined in terms of [[MapData]],
which I can't intercept access to).

(Though, Brendan says in his response that we probably shouldn't, and
should instead just pledge to only add new methods onto Map
subclasses.  I don't think that's a particularly good idea, but it
would address most of my problem.)

>>> The even simpler course is to keep Map as spec'ed and make DOM or other
>>> specs work harder. Trade-offs...
>>
>> The reason I'm really pushing on this is that it's not just DOM (we've
>> already made the trade-off there, by adding [MapClass] to WebIDL), but
>> user-space as well.  I can't make a safe Counter class that's built on
>> a Map unless I make the tradeoff above.
>
> Did you mean [[MapData]]?  [[MapData]] is just private state of the built-in object.  If you're saving that you can't safely define certain abstractions with access to private state mechanism, then I agree you.  But trying to turn [[MapData]] into an object level extension mechanism isn't the solution to your general problem.

No, I mean [MapClass].
<http://dev.w3.org/2006/webapi/WebIDL/#MapClass>  It's a WebIDL
extended attribute - the syntax has no relation to your use of [[foo]]
in the ES spec.  It declares the interface to be a Map subclass,
setting Map.prototype on its prototype chain and providing default
implementations of several Map methods for you.  (This is especially
helpful on the spec level, as you can then just define what the map
entries are and then methods like get() are auto-defined for you.)


More information about the es-discuss mailing list