Map: filter/map and more

# Dmitry Soshnikov (4 years ago)

(Maps are awesome!)

1) Transforming iteration methods

We're currently polyfillying the Map, and got some questions from devs. One of them is about transforming iteration methods, like map and filter.

Unfortunately I missed that part of the spec when it was approved, so can someone please remind/clarify -- was it an intended decision not to have Map#map, Map#filter? I can see only Map#forEach in the spec. Are maps "immutable"? -- That's fine, the map and filter return a new map.

2) Declarative syntax

The other thing to note: currently maps a lack of a nice declarative syntax. This one came from the use-case for maps for dynamic (computed) property names.

Previously, we had to allocate an empty object, and then, in the imperative style, append needed props:

var requestData = {};
requestData[Names.ID] = id;
requestData[Names.EMAIL] = email;
requestData.needsReload = true;
...
new Request(...)
  .setData(requestData)
  .send();

With computed properties of object initialisers it's much simpler and convenient:

new Request(...)
  .setData({
    [Names.ID]: id,
    [Names.EMAIL]: email,
    needsReload: true,
  })
  .send();

Then thing is: if we'd like to use maps for such use-case, it brings us back to that inconvenient imperative style of assignments (even worse, since you have to repeat that .set(...) constantly):

var requestData = new Map();
requestData.set(Names.ID, id);
requestData.set(Names.EMAIL, email);
requestData.set('needsReload', id);
...

Yes, we provide the iterable option for the constructor, and it can be rewritten as (and this can even be inlined):

var requestData = new Map([
  [Names.ID, id],
  [Names.EMAIL, email],
  ['needsReload', id]
]);

However, obviously, it's too many arrays allocation for such a simple use case.

Will it make sense having a nice declarative syntax like:

new Map({
  [Names.ID]: id,
  [Names.EMAIL]: email,
  needsReload: true,
})

It can even be done via a simple helper method that transforms this object literal with computed props to the same iterable array of array, but this, unfortunately, doesn't work with all cases like "objects as keys".

I don't have the exact idea yet of how such a syntax can look like, but it seems would be nice to have.

(unless, the use-case is not for maps, and we should use simple objects here as was shown above)

# Rick Waldron (4 years ago)

Was it an intended decision not to have Map#map, Map#filter?

Only with regard to time. I expect there will be substantial additions to Map and Set in ES7 (as long as the work is done, of course).

Will it make sense having a nice declarative syntax like:

new Map({
  [Names.ID]: id,
  [Names.EMAIL]: email,
  needsReload: true,
})

This doesn't work because Maps allow objects as keys.

# Dmitry Soshnikov (4 years ago)

I expect there will be substantial additions to Map and Set in ES7 (as long as the work is done, of course).

Hm, sounds like two copy-pasting algorithms from the same Array#map, Array#filter, or just tweaking the Map#forEach (I might be missing something).

This doesn't work because Maps allow objects as keys.

Yes, I said it myself above. That's the thing -- I'd like to thing about some special syntax maybe. Don't know yet, probably map as a construct for declarative cases like:

// Declarative
Map {
  [foo]: 1,
  bar: 2,
}

// Imperative (via constructor)
new Map([ // brrr..
  [foo, 1],
  ['bar', 2]
])
# Axel Rauschmayer (4 years ago)

Then thing is: if we'd like to use maps for such use-case, it brings us back to that inconvenient imperative style of assignments (even worse, since you have to repeat that .set(...) constantly):

var requestData = new Map();
requestData.set(Names.ID, id);
requestData.set(Names.EMAIL, email);
requestData.set('needsReload', id);
...

Note that you can chain:

var requestData = new Map()
.set(Names.ID, id)
.set(Names.EMAIL, email)
.set('needsReload', id);

Not too bad, IMO.

# Jeremy Martin (4 years ago)

Not sure if this is sufficient motivation to accelerate the timeline for adding suitable parallels from Array.prototype, but it may be worth recalling what developers did last time there were obvious "gaps" in what a native prototype provided. Array#contains, anyone?

# Rick Waldron (4 years ago)

On Wed, Oct 1, 2014 at 3:51 PM, Jeremy Martin <jmar777 at gmail.com> wrote:

Not sure if this is sufficient motivation to accelerate the timeline

It's not about "motivation", it's about realistic time constraints. TC39 has already had to push out 6 months and that was not taken well by both the community and by Ecma. Further delay is not an option.

for adding suitable parallels from Array.prototype, but it may be worth recalling what developers did last time there were obvious "gaps" in what a native prototype provided. Array#contains, anyone?

As a community we'll simply have to reject the use of code that directly modifies built-ins. This is also not a very strong argument in the ES6 world where built-ins can be subclassed.

# Axel Rauschmayer (4 years ago)

FWIW: I’ll probably use the following work-arounds. I don’t expect performance to be an issue for my applications; it’d be interesting to hear if it becomes a problem for someone.

let map2 = new Map(map1.entries().filter((key, value) => key >= 0));

let map2 = new Map(map1.entries().map((key, value) => [key * 2, value * 2]));
# Axel Rauschmayer (4 years ago)

Almost. Correct versions:

let map2 = new Map(map1.entries().filter(([key, value]) => key >= 0));

let map2 = new Map(map1.entries().map(([key, value]) => [key * 2, value * 2]));
# Rick Waldron (4 years ago)

Also, if there are written proposals, the userland extensions (if they really must be) can follow those proposals.

# Dmitry Soshnikov (4 years ago)

It's not about "motivation", it's about realistic time constraints.

Just out of curiosity: what's the realistic "out of time" issue here? Actually, I think having a ready and working algorithm draft on github gist will help discussing this faster and in realistic time frames (I've should've done before starting this thread; otherwise, these are "too abstract time-stoppers" for me -- unless you know specific big issues that will be hard to implement).

As a community we'll simply have to reject the use of code that directly modifies built-ins.

Actually the case with "borken" Array#contains which is so actively discussed, is a good example. I think very likely Map#map, and Map#filter will be added manually if not in ES6.

# Rick Waldron (4 years ago)
let map2 = new Map(map1.entries().filter((key, value) => key >= 0));
let map2 = new Map(map1.entries().map((key, value) => [key * 2, value *
2]));

entries() returns an iterator, not an array.

# Rick Waldron (4 years ago)

Just out of curiosity: what's the realistic "out of time" issue here?

If this was really pressing, why wasn't it on any meeting agendas in the last year?

The spec must be essentially finished by the next meeting (the last meeting in 2014). Finalizing the Loader details, super class instantiation (you might recall that was an issue recently), RegExp work in Annex B (may require lexical grammar changes), revising, reviewing, etc. (I know there is a lot more here)... All of this takes precedence over API surface additions, whether complete or not. Additionally, the committee is trying establish a new process for all new features. Domenic could've tried to push Array.prototype.contains into ES6, but he's following the process. tc39/ecma262

I think very likely Map#map, and Map#filter will be added manually if not in ES6.

See my response, I doubt that a one year wait until ES7 will really ruin the chances for map and filter methods. In the meantime, write the spec, get it added to tc39/ecma262 and enforce the semantics on all userland implementations. Time will fly, I promise.

# Axel Rauschmayer (4 years ago)

Ah, thanks! Then I’d wrap the result of entries() in Array.from(). In ES7, we’ll hopefully be able to use comprehensions or iterator helper functions.

# Kevin Smith (4 years ago)

entries() returns an iterator, not an array.

let map2 = new Map([...map1].map(([k, v]) => [k, v * 2])
# Brendan Eich (4 years ago)

Sweet!

# Rick Waldron (4 years ago)

Ah, thanks! Then I’d wrap the result of entries() in Array.from().

Don't need to do that either.

let map1 = new Map([[-1, -1], [0, 0], [1, 1], [2, 2]]);
let map2 = new Map([...map1].filter(([key, value]) => key >= 0));

let map3 = new Map([...map1].map(([key, value]) => [key * 2, value * 2]));

console.log([...map2]); // [[0, 0], [1, 1], [2, 2]]
console.log([...map3]); // [[-2, -2], [0, 0], [2, 2], [4, 4]]
# Rick Waldron (4 years ago)

let map2 = new Map([...map1].map(([k, v]) => [k, v * 2])

Sorry for the almost-dup, didn't see this while I was typing.

# Dmitry Soshnikov (4 years ago)

OK, FWIW:

Does it look like a vital algorithm? And if no one sees big/any issue with, can it potentially be considered for inclusion? I'm not gonna push it of course taking into account higher-pri things, however, if it seems to everyone trivial -- why not.

(Otherwise, at least it can be a source for monkey-patching the Map.prototype if it's only for ES7).

# Dmitry Soshnikov (4 years ago)

Note that you can chain:

var requestData = new Map()
.set(Names.ID, id)
.set(Names.EMAIL, email)
.set('needsReload', id);

Not too bad, IMO.

Not ideal either. Usually langs provide nice declarative syntax for such things. E.g. we have the same in the HACK language, and use it well everyday when need a map.

But this part is of course not for ES6, hope ES7-ish.

# Dmitry Soshnikov (4 years ago)

If this was really pressing, why wasn't it on any meeting agendas in the last year?

I'm sorry, as mentioned at the beginning of this thread, I unfortunately missed this part of the spec when the decisions were made. And since we just recently started to polyfill the Map in our codebase following the spec, I was surprised that neither map, nor filter are actually in the spec. I thought there were some real big issues, and wanted to clarify them. If it's only "out of time", I'd of course propose it for the agenda.

... All of this takes precedence over API surface additions, whether complete or not.

Yeah, absolutely agree that these are higher-pri.

Time will fly, I promise.

Sure, at first glance the algorithms are trivial enough as posted above.

# Brendan Eich (4 years ago)

Usually langs provide nice declarative syntax for such things.

We could definitely have Map and Set literals:

const map = {1 => "one", "two" => true, false => "three"};

const set = {<1, "two", false>};

If you still buy Harmony of My Dreams, prefix # before { to get immutable value-type forms.

I don't mind reusing => in initialiser context where : would go, but

perhaps someone sees a problem I don't. The Set literal hack of {< and >} seems necessary given object initialiser property assignment

shorthand syntax ({x, y} for {x:x, y:y}). Some kind of hack is required, yet losing { and } as outermost bracketing characters for Set seems worse than any digraph or token-pair alternative.

# Dmitry Soshnikov (4 years ago)
const map = {1 => "one", "two" => true, false => "three"};

const set = {<1, "two", false>};

Yeah, something like this (I actually like these two forms); later for ES7.

Prefix # before { to get immutable value-type forms.

Yep, why not, can be (re)considered as well I guess.

# Axel Rauschmayer (4 years ago)
const map = {1 => "one", "two" => true, false => "three"};

Would that be tricky to parse if keys can be arbitrary expressions (incl. array literals)?

I don't mind reusing => in initialiser context where : would go, but perhaps someone sees a problem I don't.

Using something other than a colon seems a good idea, to make it clear that any kind of value can be used as keys.

Another possibility:

const map = {: 1 => "one", "two" => true, false => "three" :};

const set = {. 1, "two", false .};
# Brendan Eich (4 years ago)

Would that be tricky to parse?

The issue would be arrow function expression keys:

const map = {x => x*x => "square"};

But such a key could not be found without capturing the reference:

let f;
const map = {f = x => x*x => "square"};

let g = map.get(f);
assertEq(g, f);

It's all parseable but an eyesore; but also unlikely/contrived.

Another possibility:

const map = {: 1 => "one", "two" => true, false => "three" :};
const set = {. 1, "two", false .};

Not sure what's best, but . is visually light, also used in the language in ways independent from Sets.

I don't see the need for extra delimiters for map literals -- why tax two chars each literal just for parallelism w.r.t. set literals?

# Rick Waldron (4 years ago)
# Dmitry Soshnikov (4 years ago)

Thanks, done. tc39/ecma262#13

# Domenic Denicola (4 years ago)

Thanks, done. tc39/ecma262#13

This seems less useful than adding %IteratorPrototype%.map and %IteratorPrototype.filter. It also clarifies some of the confusion about whether you are changing the values, keys, or entries (since you would need to specify explicitly).

Unless I'm missing something (quite possible!), I would prefer not to add new methods to Map and Set when they could be added to %IteratorPrototype%.

# Domenic Denicola (4 years ago)

Yeah, I'm missing something. It's the difference in code between:

var newMap = oldMap.map(([k, v]) => [k + 1, v + 1]);

versus

var newMap = new Map(oldMap.entries().map((([k, v]) => [k + 1, v + 1]);

I think I still prefer avoiding every iterable subclass adding its own map/filter/etc. in favor of people using the compositional base primitives, but at least I see the argument now.

# Dmitry Soshnikov (4 years ago)

We already have Map#forEach, and correlation with Array#forEach -> Array#map makes this constancy intuitive.

The only thing which Rick mentioned, is that Map#map should probably return a transformed [key, value] pair, that allows updating the key as well. However, need to think about it, since as mentioned on the diff [1], usually people transform only values, and to force them always return an array may be annoying (the key is not transformed in this case), and less performant in terms useless array allocation.

This case I'd probably make as Map#mapWithKey, however there other questions arise like, should the returned pair replace the existing, or create a new one, if the key is different?

({a => 10}).mapWithKey((k, v) => ['b', 20]); // {b => 20} or {a => 20, b =>

20} ?

But this is for further discussion.

# Brendan Eich (4 years ago)
var newMap = oldMap.map(([k, v]) =>  [k+ 1,  v+ 1]);

versus

var newMap = new Map(oldMap.entries().map((([k, v]) =>  [k+ 1,  v+ 1]);

But entries returns an iterator, and if we define %IteratorPrototype% to create a new instance of the right class given its |this| parameter, in this case Map and not MapIterator, then no need for the new Map() wrapper.

I think I still prefer avoiding every iterable subclass adding its own map/filter/etc.

I point out in www.w3.org/Bugs/Public/show_bug.cgi?id=26973#c3 that the lack of Map.prototype.{map,filter,...} doesn't bite when you use for-of, but of course will be missed when you want to call someMap.filter directly. Which is reasonable to do!

# Dmitry Soshnikov (4 years ago)
({a => 10}).mapWithKey((k, v) => ['b', 20]); // {b => 20} or {a => 20, b
=> 20} ?

Whoops, nevermind actually, since we don't mutate the original map (the this), but rather return a new one, it should always be just {b => 20},

and a key is ignored in the transformed map.

Dmitry

# Domenic Denicola (4 years ago)

From: Brendan Eich [mailto:brendan at mozilla.org]

But entries returns an iterator, and if we define %IteratorPrototype% to create a new instance of the right class given its |this| parameter, in this case Map and not MapIterator, then no need for the new Map() wrapper.

This seems like it'd involve too much machinery and give unexpected results.

I'd expect instance.map(...) to return another one of instance. That is, given a MapIterator i, I'd expect i.map(...) to be another MapIterator, and not a Map like you are proposing.

Giving a MapIterator also has the advantage of preserving laziness. I like how in

var newMap = new Map(oldMap.entries().map((([k, v]) =>  [k + 1,  v+ 1]);

The Map constructor call gives a clear delineation of where the iterator is being consumed, and the laziness is flattened down. Whereas if .entries().map(...) gave a Map instance straightaway, the option of keeping things lazy would be gone.

# Brendan Eich (4 years ago)

This seems like it'd involve too much machinery and give unexpected results.

I'd expect instance.map(...) to return another one of instance. That is, given a MapIterator i, I'd expect i.map(...) to be another MapIterator, and not a Map like you are proposing.

Right, I was smoking something bad. The uniformity you describe is clearly the right solution.

This means Map.prototype.map etc. -- or %ItereablePrototype%.map if not Array.prototype.map etc. -- are the eager collection-generic companions.

# Brendan Eich (4 years ago)

Bear with me, trying to develop a half-thought. To say it with code:

Object.defineProperty(%IteratorPrototype%, 'map', {
     value: function (fun) {
         return function* (iter) {
             for (let [key, val] of iter) {
                 yield fun(val, key);
             }
         }(this.clone());
     },
     configurable: true,
     enumerable: false,
     writable: true
});

Note the this.clone() call. This is novel, and required to avoid exhausting the receiver iterator.

Rather than %IterablePrototype%, which is too generic a name (it does not imply a key/value map), we need something such as %MaplikePrototype% or %CollectionPrototype%:

Object.defineProperty(%CollectionPrototype%, 'map', {
     value: function (fun) {
         let result = new this.constructor();
         for (let [key, val] of this) {
             result.set(key, val);
         }
         return result;
     },
     configurable: true,
     enumerable: false,
     writable: true
});

The Collection protocol thus consists of at least .constructor, @@iterator, .set. (Is there a better way to clone? Don't want a new protocol where an old one will suffice!) This Collection protocol would include Map and maplikes but leaves out Set, Array, Object -- appropriately.

We could just use FP-style map, filter, etc. APIs, but as argued among TC39ers, OOP wins in JS: (a) it matches built-ins; (b) it composes left to right, not inside out.

This argument implies a %CollectionPrototype% object full of protocol-generic methods, to avoid rewriting collection methods all over the place (wherever there is a maplike), or borrowing Map.prototype.* references by hand and monkeypatching them into maplike prototypes.

Unless I'm mistaken, the OOP-ness also implies other stuff, like a way to clone an iterator.

I hope this makes sense. Comments welcome.

# Brendan Eich (4 years ago)

Caffeinating still, obvious bugfix below:

Brendan Eich wrote:

result.set(key, val); 
result.set(key, fun(val, key));

I call the mapfun in these examples with (val, key) -- could use the e-i-c convention from the Array extras but I was in a hurry (wherefore this bug).

# Domenic Denicola (4 years ago)

Note the this.clone() call. This is novel, and required to avoid exhausting the receiver iterator.

This is noteworthy and worth keeping in mind regardless of the rest of the discussion. I think I would be fine having %IterablePrototype%.map exhaust the iterator. I would .clone() manually myself if I didn't want that. But in general anytime I loop through the iterator---either directly with for-of, or indirectly with .map and friends---I would expect exhaustion.

We could just use FP-style map, filter, etc. APIs, but as argued among TC39ers, OOP wins in JS: (a) it matches built-ins; (b) it composes left to right, not inside out.

I am still hoping the bind operator gives us a good middle ground. E.g.

import { map } from "js/iterables";

var newMap = oldMap::map(([k, v]) => [k + 1, v + 1]);

Here I guess map would look something like

function map(...args) {
  return new this.constructor(this.entries().map(...args));
}

(Note that I use new this.constructor(entriesIterable) instead of your var m = new this.constructor() plus m.set(...). Unclear which is better.)

Comments welcome.

My first thought is that it seems a little far out there given that we're talking about the net benefit being reducing

var newMap = new Map(oldMap.entries().map((([k, v]) => [k + 1, v + 1]);

to

var newMap = oldMap.map(([k, v]) => [k + 1, v + 1]);

My second thought is that we might also want to consider approaching this from the perspective of traits, see e.g. an earlier discussion about Set.

But, collections do need some love, so it's good that we're thinking about this.

# Brendan Eich (4 years ago)

I would expect exhaustion.

For iteration, sure. For map or other functional (pure?!) APIs? Yikes.

If in ES7 we build up the duality between iterables and observables, add for-on, pave the way toward FRP, then map as a protocol (part) should be pure with respect to its receiver, ISTM.

the bind operator

More elaborate constructor protocol, but if it's part of maplike, may be ok.

Problem is :: vs. just dot, and now you have two problems. I'm actually good with FP inside-out, could learn to love :: if we add it, but the main objection IMHO is two-problems. OOP means just dot.

My first thought is that it seems a little far out there

Fair, just trying to develop a thought on es-discuss. I'm not sure we should do anything in TC39 about this. I want future underscore.js libraries to tread those cowpaths we ultimately pave.

Also the ES7 iterable/observable duality and the DOM maplike demands combine to push for paving sooner. We should be minimalists as you suggest, though.

My second thought is that we might also want to consider approaching this from the perspective of traits

Traits are great -- we lost a live, championed proposal at some point on the road to ES6. Who will revive?

# Domenic Denicola (4 years ago)

From: Brendan Eich [mailto:brendan at mozilla.org]

For iteration, sure. For map or other functional (pure?!) APIs? Yikes.

If in ES7 we build up the duality between iterables and observables, add for-on, pave the way toward FRP, then map as a protocol (part) should be pure with respect to its receiver, ISTM.

This is iterables vs. iterators IMO. Iterators are inherently stateful and I'd expect anything that uses them to consume them, even something named "map". Iterables of course would not be.

# Jason Orendorff (4 years ago)

I agree. It's inherent in the python-like design we're using for iteration.

It is not so jarring in practice, even for people with the classical training to recognize what a farce it is. :) Many uses of itertools in python, though blatantly stateful in implementation and operation, still "feel" functional, because the iterator being consumed is a temporary, something like mydict.iterkeys(). The mutation isn't observable if there's no other reference to the iterator object.

# Brendan Eich (4 years ago)

Good to known (and I had heard this via Python community channels already). Still feels wrong, given "name polymorphism" on map. Perhaps we just plow ahead.

Still seems like someone (at some layer) will want iter.clone(). Vouch?

# Kevin Smith (4 years ago)

Problem is :: vs. just dot, and now you have two problems. I'm actually good with FP inside-out, could learn to love :: if we add it, but the main objection IMHO is two-problems. OOP means just dot.

That reminds me:

esdiscuss.org/topic/merging-bind-syntax-with-relationships

I know, OOP wins and all, but the downside is that it puts all the burden onto TC39 for defining a complete iterator API. The double-colon is a bit nasty-looking, though... Maybe we should consider single-arrow?

For example:

import { slice, forEach } from "my-userland-iterator-lib";

gimmeIterator()->slice(10, 20)->forEach(x => console.log(x));

Or for private fields:

import { makePrivate } from "private-fields";

const _x = makePrivate();
const _y = makePrivate();

class Point {
    constructor(x, y) {
        this->_x = x;
        this->_y = y;
    }
}

In any case, I think a merged relationships (i.e. private fields) + binding mechanism would be a huge win for the language.

# Andrea Giammarchi (4 years ago)

Coming form other languages, double colons is used as a "static" thing, although we don't need (?) this convention in ES6 classes, and I agree it looks ugly anyway.

I wonder if here :> would work instead, because I'd personally find any

other usage of the thin arrow -> confusing *

  • I personally see -> as a shortcut for function
# Tab Atkins Jr. (4 years ago)

Should the returned pair replace the existing, or create a new one, if the key is different?

Mapping produces a new object, so the answer to that is luckily trivial.

# Kevin Smith (4 years ago)
  • I personally see -> as a shortcut for function

I'm a C sympathizer, so I don't see it the quite the same, but I understand. Regardless of the token, I feel like a generalized ref_get/set mechanism would be great.

# Mark S. Miller (4 years ago)

On Tue, Oct 7, 2014 at 8:51 AM, Kevin Smith <zenparsing at gmail.com> wrote:

Problem is :: vs. just dot, and now you have two problems. I'm actually good with FP inside-out, could learn to love :: if we add it, but the main objection IMHO is two-problems. OOP means just dot.

That reminds me:

esdiscuss.org/topic/merging-bind-syntax-with-relationships

Hi Kevin,

Somehow I had missed that post until now. Interesting! For the sake of clarity while examining alternatives, let's assume for the moment the following infix operators:

infix "::" for bind alone as in strawman:bind_operator

infix "@" for relationships alone, as in strawman:relationships

infix "->" for bind+relationships as you propose here.

My question is: If we have "->", how much need would there still be for "::" or "@"? If "->" can subsume the practical need for either or both of

the others, that would be interesting.

We should postpone any further bikeshedding on which token to use for which meaning until we understand better which ones we actually need.

# Kevin Smith (4 years ago)

My question is: If we have "->", how much need would there still be for "::" or "@"? If "->" can subsume the practical need for either or both of the others, that would be interesting.

That was the idea, but it's been a while since I went through that exercise. I need to review.

# Dmitry Soshnikov (4 years ago)

I think it's definitely worth discussing the generic collection prototype, however at the meantime, an updated version of the mapping function, here: gist.github.com/DmitrySoshnikov/a218700746b2d7a7d2c8

  • Moved the mapping to the abstract MapTransform
  • Reused it for Map#map and Map#mapEntries (which updates the keys as well)
# Dmitry Soshnikov (4 years ago)

On Mon, Oct 6, 2014 at 9:37 AM, Brendan Eich <brendan at mozilla.org> wrote:

Brendan Eich wrote:

This meansMap.prototype.mapetc. -- or %ItereablePrototype%.map if not Array.prototype.map etc. -- are the eager collection-generic companions.

Bear with me, trying to develop a half-thought. To say it with code:

Object.defineProperty(%IteratorPrototype%, 'map', {
    value: function (fun) {
        return function* (iter) {
            for (let [key, val] of iter) {
                yield fun(val, key);
            }
        }(this.clone());
    },
    configurable: true,
    enumerable: false,
    writable: true
});

Note the this.clone() call. This is novel, and required to avoid exhausting the receiver iterator.

Rather than %IterablePrototype%, which is too generic a name (it does not imply a key/value map), we need something such as %MaplikePrototype% or %CollectionPrototype%:

Object.defineProperty(%CollectionPrototype%, 'map', {
    value: function (fun) {
        let result = new this.constructor();
        for (let [key, val] of this) {
            result.set(key, val);
        }
        return result;
    },
    configurable: true,
    enumerable: false,
    writable: true
});

The Collection protocol thus consists of at least .constructor, @@iterator, .set. (Is there a better way to clone? Don't want a new protocol where an old one will suffice!) This Collection protocol would include Map and maplikes but leaves out Set, Array, Object -- appropriately.

As long as it can be generic enough for Map, Set and .. (what else?) -- I actually like this one. Can try specifying it in a separate diff and will get back to you.

We could just use FP-style map, filter, etc. APIs, but as argued among TC39ers, OOP wins in JS: (a) it matches built-ins; (b) it composes left to right, not inside out.

This argument implies a %CollectionPrototype% object full of protocol-generic methods, to avoid rewriting collection methods all over the place (wherever there is a maplike), or borrowing Map.prototype.* references by hand and monkeypatching them into maplike prototypes.

Yeah.

Unless I'm mistaken, the OOP-ness also implies other stuff, like a way to clone an iterator.

I hope this makes sense. Comments welcome.

Yep, thanks!

Dmitry

# Brendan Eich (4 years ago)

Dmitry Soshnikov wrote:

The Collection protocol thus consists of at least .constructor,
@@iterator, .set. (Is there a better way to clone? Don't want a
new protocol where an old one will suffice!) This Collection
protocol would include Map and maplikes but leaves out Set, Array,
Object -- appropriately.

As long as it can be generic enough for Map, Set

Set methods include add, whereas Map has set. Set lacks a get method, whereas Map of course has one. So not the same protocol.

# Dmitry Soshnikov (4 years ago)

On Wed, Oct 8, 2014 at 3:09 PM, Brendan Eich <brendan at mozilla.org> wrote:

Dmitry Soshnikov wrote:

The Collection protocol thus consists of at least .constructor,
@@iterator, .set. (Is there a better way to clone? Don't want a
new protocol where an old one will suffice!) This Collection
protocol would include Map and maplikes but leaves out Set, Array,
Object -- appropriately.

As long as it can be generic enough for Map, Set

Set methods include add, whereas Map has set. Set lacks a get method, whereas Map of course has one. So not the same protocol.

I see, yeah, it's doable, and potentially they can be handled via abstract operations with checking kind of a collection (IsSet, IsMap), and setting needed entries (AddEntry would cover set for maps, and add for Sets), and GetEntry would return needed thing since sets are just backed by maps. However, this seems not a big win in terms of constant runtime checks for this, and probably having a separate algorithms are better (even if some parts are repeated).

If you want to have just an explicit protocol/interface for "MapLike"s and "SetLikes" (i.e. with any explicit set method, and stuff which can be implemented at user-level by any object), it's possible. Although, this interface implementation can also be just replaced with sub-classing of the Map, and the same algorithm works. It's just a difference b/w duck-typing (the protocol, that any object may implement regardless its hierarchy), or the inheritance. Will think about it, and maybe will come up with a spec.

Dmitry

# Dmitry Soshnikov (4 years ago)

Is this something what is ready for discussion on a meeting (in particular, tomorrow's meeting)?

Regardless, whether the map and set will be directly on the Map.prototype or on some generic %CollectionPrototype%, the user-level API will stay the same, right? And will be used as:

myMap.map((v, k) => { ... });

I was just curious, is it already the time when it can be confirmed on a meeting, that the API will likely be this (assuming we have map.forEach already)? We'd like already to extend the Map and Set API (by extending the ES6 base implementation) with several new methods, and start using them.

If this spec gist.github.com/DmitrySoshnikov/a218700746b2d7a7d2c8 is good enough to discuss it and can be confirmed, will it be a good time on the following meeting? This PR for agenda wasn't merged for almost a month: tc39/agendas#53

If it can be confirmed already now, and no actual meeting discussion is needed for this, I'm fine with this as well.

The only thing that may not confirm it, is using that new binding method :: operator and iter tools:

import {map} form 'iter-tools'

map::map(...);

But this as I understand is not even in early stage of a proposal (and again, having dependency/correlation on map.forEach probably the map and filter will nevertheless be used as just map.map).

P.S.: besides these two methods, maps and sets API is pretty basic at the moment and doesn't have many "every-day to use" which are available in other languages in these collections: like the most obvious "intersect" or "union" operations for sets, etc. However, we can start with "map" and "filter" yet ;)

Dmitry

# Brendan Eich (4 years ago)

Dmitry Soshnikov wrote:

Is this something what is ready for discussion on a meeting (in particular, tomorrow's meeting)?

You put something on the agenda, cool.

tc39/agendas/blob/master/2014/11.md

# Dmitry Soshnikov (4 years ago)

(For the history of this thread to refer to it later)

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collect(); // returns a new map after all transforms

The forEach method is the only which will still exist on Map.prototype, and the committee agreed it's fine.

At the same time, the forEach can be called on iterator as well:

  map
    .entries()
    .map(...)
    .forEach(...);

Dmitry

# Tab Atkins Jr. (4 years ago)

On Wed, Nov 19, 2014 at 4:49 PM, Dmitry Soshnikov <dmitry.soshnikov at gmail.com> wrote:

(For the history of this thread to refer to it later)

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collect(); // returns a new map after all transforms

Sounds good! (I always end up calling one of the iterators on python dicts anyway, since I can never remember what the default iterator behavior is.)

I presume that .collect() expects an iterator of [k,v] pairs or something? Also: this is the first I've heard of .collect(). I presume that'll show up in the spec?

# Axel Rauschmayer (4 years ago)

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collect(); // returns a new map after all transforms

Convenient, but this pattern couldn’t be extended to new Map classes or other collections. I see two alternatives.

First, use the Map constructor. I don’t find this too bad and it’s self-explanatory.

new Map(map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... }));

Second:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collectInto(new Map());

collectInto(coll) invokes coll.set(k,v) for each pair [k,v] in the sequence. It returns coll.

# Mark Volkmann (4 years ago)

Where can I read about the rationale to not put those methods on the Map prototype? I'm curious why that was okay for the Array class, but not okay for Map and Set.

# Tab Atkins Jr. (4 years ago)

On Thu, Nov 20, 2014 at 10:32 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collect(); // returns a new map after all transforms

Convenient, but this pattern couldn’t be extended to new Map classes or other collections. I see two alternatives.

First, use the Map constructor. I don’t find this too bad and it’s self-explanatory.

new Map(map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... }));

Second:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collectInto(new Map());

collectInto(coll) invokes coll.set(k,v) for each pair [k,v] in the sequence. It returns coll.

+1. This also gives us a pseudonym for Python's .update() operation, which adds one dict into another, and which I use relatively often to merge dictionaries together. Where Python would say m1.update(m2) (m1 now contains all of m2's data), JS would say m2.entries().collectInto(m1).

This is more powerful than just using the constructor, as it allows you to update a dict that already has information in it, and adding another function to a chain of transformations is slightly easier to do than wrapping it in a function call.

# Domenic Denicola (4 years ago)

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

Sounds good! (I always end up calling one of the iterators on python dicts anyway, since I can never remember what the default iterator behavior is.)

Yeah. There is a separate idea that we should probably define Map.prototype.{map, filter, etc.} as behaving the same way as the default iterator (which in this case is .entries()). There's a few ideas on how to do this, e.g. specialized Map methods, or put a "Collection prototype" between Map.prototype and Object.prototype that generically delegates to the default iterator, or...

I presume that .collect() expects an iterator of [k,v] pairs or something? Also: this is the first I've heard of .collect(). I presume that'll show up in the spec?

The idea of .collect() is that when the iterator is created, it's told how to collect; in this case it would be using new Map I think. (Although there was talk of Map.from at the meeting, which I guess would be an additional API.) Since the iterator in this example does yield [k, v] pairs, and new Map accepts an iterable of [k, v] pairs, this will work nicely.

But yes, right now %IteratorPrototype% is entirely empty as just a placeholder for ES6. It needs map, filter, forEach, reduce, as well as collect. Design work to be done :)

# Axel Rauschmayer (4 years ago)

On 20 Nov 2014, at 1:49 , Dmitry Soshnikov <dmitry.soshnikov at gmail.com> wrote:

(For the history of this thread to refer to it later)

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
  .entries() // returns an iterator
  .map((v, k, m) => { ... })
  .filter((v, k, m) => { ... })
  .collect(); // returns a new map after all transforms

Question – shouldn’t this example be written as follows?

map
  .entries() // returns an iterator
  .map(([k,v], i, m) => { ... })
  .filter(([k,v], i, m) => { ... })
  .collect(); // returns a new map after all transforms

Otherwise, I suggest to change the names (e.g. to mapPairs and filterPairs).

# Domenic Denicola (4 years ago)

From: Axel Rauschmayer [mailto:axel at rauschma.de]

Question – shouldn’t this example be written as follows?

Yes.

# Tab Atkins Jr. (4 years ago)

On Thu, Nov 20, 2014 at 10:42 AM, Domenic Denicola <d at domenic.me> wrote:

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

I presume that .collect() expects an iterator of [k,v] pairs or something? Also: this is the first I've heard of .collect(). I presume that'll show up in the spec?

The idea of .collect() is that when the iterator is created, it's told how to collect; in this case it would be using new Map I think.

Interesting idea, but I'm not sure how it's supposed to work in practice. Is this metadata exposed somehow, so you can pass it along to new iterators you create in custom iterator-algebra methods? How does this work with iterator-algebra methods implemented as generators?

# Domenic Denicola (4 years ago)

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

Interesting idea, but I'm not sure how it's supposed to work in practice.

Doesn't answer all of your questions, and is probably half-assed in various ways, but here is some stuff that we came up with in some live-coding during the meeting:

Map.prototype.entries = function () {
  return new MapIterator({ collectAs: this.constructor[Symbol.species] });
};
class MapIterator extends Iterator {
  constructor({ collectAs }) {
    super();
    this[Symbol.collectAs] = collectAs;
  }
}
Iterator.prototype.collect = function () {
  return this[Symbol.collectAs].from(...);
};
myMap.entries()                      // proposal: you can remove .entries()
  .filter(([k, v]) => k % 2 === 0)
  .map(([k, v]) => [k * 2, v])
  .forEach(...);
  // or .reduce(...) also forces
  // or for-of
  // or .collectAs(Array)
  // or .collect(), which uses [Symbol.collectAs] as a default
# Andy Wingo (3 years ago)

On Thu 20 Nov 2014 01:49, Dmitry Soshnikov <dmitry.soshnikov at gmail.com> writes:

At the meeting it was decided not to go with map and filter sitting on Map.prototype, but instead to use iterators in the way like:

map
.entries() // returns an iterator
.map((v, k, m) => { ... })
.filter((v, k, m) => { ... })
.collect(); // returns a new map after all transforms

Is there a nice way to do this for the default iterator? This is pretty terrible:

[1,2,3][Symbol.iterator]().map(x=>x+1)

Also, I thought that map, filter, and such were to be on the Iterator.prototype. That precludes multi-arg map functions, doesn't it?

# Domenic Denicola (3 years ago)

From: Andy Wingo [mailto:wingo at igalia.com]

Is there a nice way to do this for the default iterator?

esdiscuss.org/topic/map-filter-map-and-more#content-57

Also, I thought that map, filter, and such were to be on the Iterator.prototype. That precludes multi-arg map functions, doesn't it?

esdiscuss.org/topic/map-filter-map-and-more#content-59