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

Allen Wirfs-Brock allen at wirfs-brock.com
Fri Aug 2 12:06:57 PDT 2013


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.

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

'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.

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. 

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

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.

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

Allen


More information about the es-discuss mailing list