Array subclassing, .map and iterables (Re: Jan 30 TC39 Meeting Notes)

Claus Reinke claus.reinke at talk21.com
Tue Feb 12 07:36:51 PST 2013


[to limit the length of my reply, I had to avoid responding to every 
 detail, trying to answer the gist of your message instead; please let 
 me know if I missed anything important]

> Of course, you might argue that I could just call it like:
> 
>  NodeList.from( [ "div", "span", "p" ].map(nodeName =>
> document.createElement(nodeName)) );

Indeed, this would be my preferred choice. It would be more modular
than packaging the combination of container change and element
conversion into a single operation.

However, I understand the conflict between type-changing element 
maps and element-type-constrained containers. 

Let me change the example to bring out this point: if we convert from 
an array of Int32 to an array of Double, we cannot map on the source 
array nor can we map on the target array. So we do have to map the 
elements in transit, after extracting them from the Int32 source and 
before placing them in the Double target, preferably without creating
an intermediate Array.

Since the container change (.from()) is specced via iterators, I suggest
to ensure support for .map() on iterators and map the elements in the
iteration, after extraction from source, before integration into target.

Integrating the element map into the container conversion (.from())
instead, which is a static method to enforce target-type specification,
solves the issue you were trying to address, for Array subclasses, but
it leaves us with a host of residual issues:

- there are now two operations for one generic task, 
     Array.prototype.map( <function> )    // type-preserving
    <TargetClass>.from( <source>, <function> ) // type-changing

- the two -clearly related- operations do not have a common interface,
    in fact, one is an object method, the other a static/class method

- the latter operation is really a family of operations, and the static
    type prefixes of the family members are difficult to abstract over 
    (do I try to get the target type from the target object context or 
    from the function result, or do I force the user to pass it in at 
    every call site)

- even with this additional complexity, we still do not have support
    for mapping over the elements of other, non-Array containers

I suspect that the problem of establishing target container types 
is separate from the element mapping, so I would like to keep
.from() and .map() separate. But even in the merged design
programming will be awkward.

> ...But the "arraylike" or "iterable" might not have a .map() method 
> of its own, which will cause issues if I'm in a JS-target transpilation 
> scenario...

And I would like not only to root out any "arraylike" or "iterable"
that do not support .map(), but would also like to extend the reach
of .map() to other cases where it makes sense (I've listed examples
in previous messages).
 
> (function( root ) {
>  root.converter = function( ctor, iterable, map ) {
>    return this[ ctor ].from( iterable, map );
>  }.bind(root);
> }( this ));

What you're saying here is that (1) .from() should support .map()-like
functionality for all iterables (even if they do not support .map()), that 
(2) we can't use .map() because it may not be supported for all iterables 
and the 'map' parameter might be type-changing, and that (3) you 
don't know how to get the target type generically, so it'll have to be 
passed in at each call site. 

None of this is promising when I think of writing generic code that
employs mapping over different container types, even if we assume
that the mapping .from() replaces .map() as the general interface.

Are you going to pass around 'ctor's as dynamic type hints? Since
we need the target classes, we can't even extract the class from
the source arrays. This, and the inability to de-structure things
like Int32Array into its components, are among the outstanding 
language design issues generated in this area.

>> My point was that map is far more widely useful, not limited to
>> Array (Array.prototype.map), and not limited to Array construction
>> from Iterables (Array.prototype.from with second parameter). 
>> Consider map on event emitters, on promises, on exceptions, on 
>> generators, ..
>>
>> I don't have an alternative solution that would cover all use cases 
>> in ES uniformly, because the existing solutions in other languages
>> do not translate directly.
>> However, I wanted to ring a warning bell that adding a different 
>> partial solution for every new use case is not going to scale well 
>> (especially with things being so difficult to change once they are 
>> in ES), and misses potential for writing generic library code.
> 
> Can you show an example of this?

Example of what (can't resolve 'this';-)? I listed several examples of 
classes that I'd like to see map() support on. You gave an example 
of how you couldn't write generic code using map() because not 
all relevant classes support that method (using .from() on iterables 
doesn't work, either). If you mean difficulties of evolving ES designs
after release, think no further than existing code inheriting from
Array.prototype.

If you mean the code pattern and language support in general, it is 
just "programming to interfaces", applied to the interface supporting 
map. The pattern was popularized for OOP in the "Design Patterns" 
book ( http://en.wikipedia.org/wiki/Design_Patterns ), but is also 
standard in functional languages. 

Readers here tend to be familiar with OOP, so I listed an example 
earlier of language support for the map interface (the Functor 
typeclass) in Haskell, which is possibly the most widely implemented 
interface there (and shows language support for fine-grained
interface-based reuse - a type can support interfaces based on
being a container, based on containing a certain kind of element,
or based on supporting other interfaces; e.g., if you have a type
that is a container, supports 'map', and contains elements that
support '+', then you can write generic code for adding the
container elements together, without having to commit to any
specific container- or element-type; and that isn't half of it).
 
>> Implementations can fuse .from().map() as well as .map().from();
> 
> .from()  is a static method, so .map().from() won't work.

Thanks, I meant .from().map() (map after from) and .from( .map() )
(from after map).

Claus

 


More information about the es-discuss mailing list