01:13 | <ljharb> | i didn’t realize that. It shouldn’t use instanceof - like everything else in the language, it should use a brand. |
01:17 | <bakkot> | no, it's an interface |
01:17 | <bakkot> | it's not a class |
01:17 | <bakkot> | I mean, it is also a class |
01:18 | <bakkot> | but there is no particular reason a userland instance needs to have the brand |
02:14 | <snek> | i didn’t realize that. It shouldn’t use instanceof - like everything else in the language, it should use a brand. |
02:15 | <snek> | or if you subscribe to the other idea, "an object inheriting Iterator.prototype" |
03:35 | <ljharb> | objects in the language that inherit from a prototype also have a brand, thats checked in places. |
03:38 | <snek> | no i like |
03:38 | <snek> | i do not understand the mechanic by which this would work |
17:17 | <ljharb> | anything that's instanceof Iterator in the language would have an internal slot, which is what Iterator.from would check |
17:28 | <bakkot> | in the language or, presumably, the web platform? |
17:28 | <bakkot> | I suppose that would be possible but... why? |
18:22 | <snek> | anything that's |
18:23 | <snek> | oh are you saying Iterator.from would be the thing that produces objects with that slot |
18:25 | <snek> | technically the new object it returns has [[Iterated]] |
18:25 | <snek> | but |
18:25 | <snek> | it doesn't have to return the new object |
18:26 | <snek> | like if i run Iterator.from({ next() {}, __proto__: Iterator.prototype }) , it should return that argument unchanged |
19:26 | <ljharb> | that doesn’t seem like what it should be doing at all. |
19:27 | <ljharb> | Array.from doesnt pass through return an arraylike object that has a proto of Array.prototype - it returns a proper array. So should Iterator.from. |
19:28 | <bakkot> | the definition of "proper iterator" is "inherits from Iterator.prototype", in this context |
19:44 | <ljharb> | i don't think that is or should be the definition |
20:00 | <bakkot> | why? |
21:08 | <snek> | Iterator is not a concrete thing, I'd rather rename the method if that's the problem |
21:15 | <ljharb> | the whole point of Iterator.from is that it's becoming a concrete thing |
21:15 | <snek> | no its just that its giving it the prototype |
21:16 | <snek> | its a convenience for the methods |
21:16 | <ljharb> | … and the methods do a wrap (to something with the slot) if they're .call ed on a random object? |
21:17 | <snek> | i don't understand what this means |
21:17 | <ljharb> | the [[Iterated]] slot you mentioned |
21:18 | <snek> | [[Iterated]] is just used for %WrapForValidIteratorPrototype%.next/return |
21:18 | <ljharb> | how can the next method retain internal state unless it has a place to put it? |
21:19 | <snek> | WrapForValidIteratorPrototype objects don't have any internal state |
21:19 | <snek> | [[Iterated]] is just the object they wrap |
21:19 | <snek> | well, the iterator record for it |
21:20 | <ljharb> | ok so like map . it has to hold on to the original iterator, and to the callback |
21:21 | <ljharb> | so presumably the iterator object returned by map holds that, in an internal slot |
21:21 | <snek> | they use closures |
21:22 | <ljharb> | so the iterator object doesn't use a shared next method, it makes a new one each time map is called? |
21:22 | <snek> | uhh |
21:22 | <snek> | it uses %IteratorHelperPrototype%.next |
21:22 | <ljharb> | right. but that's a shared method. |
21:22 | <snek> | which is just %GeneratorPrototype%.next but fancy |
21:22 | <ljharb> | so it can't close over something that's unique to a given map call |
21:23 | <ljharb> | because it exists before map is ever called. |
21:23 | <ljharb> | so where is that state held so that next can access it? |
21:23 | <snek> | same place that normal generators store it |
21:23 | <snek> | the only thing is |
21:24 | <snek> | you can grep for [[GeneratorBrand]] in the spec |
21:24 | <ljharb> | right but that's in a slot on the generator instance, no? |
21:24 | <snek> | https://tc39.es/ecma262/#sec-generatorvalidate |
21:24 | <snek> | this is the only way it tells them apart |
21:24 | <snek> | but that just tells you if it was a generator helper method |
21:24 | <snek> | tbh i'm not sure why we separate them at all, they just call GeneratorResume |
21:25 | <ljharb> | right. and [[GeneratorContext]] and [[GeneratorState]] are slots on the iterator object that hold the state |
21:25 | <snek> | yeah its a generator |
21:25 | <ljharb> | right |
21:26 | <ljharb> | and GeneratorValidate brand-checks the iterator. it doesn't matter what its [[Prototype]] is, it matters if it has the expected slots. |
21:26 | <snek> | ye |
21:26 | <ljharb> | so i assume that Iterator Helper methods all do the same validation |
21:26 | <snek> | iterator helper methods do not know about those brand checks, they perform them via calling stuff like GeneratorResume |
21:26 | <ljharb> | right but they still exist |
21:26 | <snek> | sure |
21:27 | <ljharb> | which means that { __proto__: Iterator.prototype } would fail that check, and not be a usable receiver with the Iterator Helper methods. which means Iterator.from musn't ever return it. |
21:27 | <ljharb> | which means that Iterator.from always returns a brand-checkable object, one that GeneratorValidate will accept. |
21:27 | <snek> | uhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh |
21:27 | <snek> | no |
21:27 | <ljharb> | what am i missing there? |
21:28 | <snek> | assuming this let iterWithGeneratorSlots = [].values().map(x => x); |
21:28 | <snek> | Iterator.from(iterWithGeneratorSlots) passes it through unchanged because it inherits from %Iterator.prototype% |
21:28 | <snek> | but |
21:28 | <snek> | if you do Iterator.from({ next() {}, __proto__: Iterator.prototype }) it also passes it through unchanged |
21:29 | <snek> | that's not doing any generator stuff |
21:29 | <snek> | if you do Iterator.from({ next() {} }) that creates a %WrapForValidIteratorPrototype% object and returns it |
21:29 | <ljharb> | ok but what happens with Iterator.from({ next() {}, __proto__: Iterator.prototype }).map(x => x) ? |
21:29 | <snek> | but that still doesn't have any generator slots |
21:29 | <snek> | cuz its not a generator |
21:29 | <snek> | its just a stupid wrapper |
21:29 | <snek> | ok but what happens with |
21:30 | <ljharb> | ok, and .map doesn't care if its receiver has the slots? |
21:30 | <snek> | no it does not |
21:30 | <ljharb> | oof |
21:30 | <snek> | all it cares is that the receiver is an iterator |
21:30 | <snek> | https://gc.gy/130118458.png |
21:32 | <snek> | this is why { next() {}, __proto__: Iterator.prototype } is valid |
21:32 | <snek> | with protocols you have to assume that the object is already created as some other shape, you're augmenting them with the functionality, implementing the protocol on top of something else |
21:33 | <ljharb> | i don't understand why, if everyone's going to Iterator.from() anyways to get the methods, we wouldn't want to unconditionally wrap - ie, to unconditionally .map(x => x) |
21:33 | <snek> | i think its pretty rare that you would need to use Iterator.from |
21:33 | <ljharb> | that way we have a consistent "real iterator" definition |
21:34 | <ljharb> | i think you'll want to do it any time you're dealing with a user-supplied iterator. which will be frequent. |
21:34 | <snek> | i mean, those are rare lol |
21:34 | <ljharb> | you'd want to maximally accept and normalize all possible iterators |
21:34 | <ljharb> | that's what Promise.resolve is good for too |
21:34 | <snek> | yeah i mean |
21:34 | <ljharb> | it normalizes thenables into Real Promises, which are always preferred |
21:34 | <snek> | if you're you, writing some sort of complex fancy library that has to deal with the world |
21:34 | <ljharb> | similarly i'd always want a Real Iterator, not just an "iterator" |
21:34 | <snek> | go for it |
21:35 | <ljharb> | this isn't just a "my unique coding style" thing tho, this is any package |
21:35 | <ljharb> | anything that accepts an iterator. just like anything that accepts a thenable always uses Promise.resolve or await to normalize it first into a real promise |
21:35 | <ljharb> | Iterator.from will be exceedingly common imo. |
21:35 | <snek> | its a nice convenient method yeah |
21:35 | <snek> | i was just pointing out |
21:35 | <snek> | its superfluous in 99% of cases |
21:36 | <snek> | so we made it not add extra overhead |
21:36 | <ljharb> | i don't think that's true |
21:36 | <ljharb> | i think 99% of cases will be dealing with untrusted iterators |
21:36 | <ljharb> | the case where you're iterating your own iterator are likely to be far rarer. (where "your own" doesn't deal with who the author is, but which chunk of code owns it) |
21:37 | <ljharb> | "extra overhead" isn't a real problem (it can be optimized away), but a footgun like "sometimes it doesn't wrap" imo would be |
21:37 | <snek> | the genesis of this proposal was the observation that all web and js platform iterators already inherit from a shared %IteratorPrototype% |
21:37 | <snek> | and the remaining weird ones often use the (function*(){})().next().__proto__ trick or whatever it is |
21:37 | <ljharb> | then whose don't need to wrap because they'll have the slots to begin with, so it doesn't matter what Iterator.from does for them |
21:38 | <ljharb> | why are we concerned with "overhead" for the highly unlikely case of someone doing { __proto__: Iterator.prototype } ? |
21:38 | <ljharb> | unlikely still means nonzero, and having to deal with that complexity (in the current case) is far more dangerous than "it's a bit slower if you do something super weird" (in the "always wrap" case) |
21:39 | <snek> | i am lost now |
21:40 | <snek> | the overhead is when you wrap something that is already matching the "iterator with prototype" definition, which is most iterator objects flying around |
21:44 | <ljharb> | anything created from the language or a generator or the web would/could already have the slots it needs. It’d only be a custom iterator implementation that’d need wrapping. How common are those? |
21:44 | <snek> | most custom ones already inherit from Iterator.prototype |
21:44 | <snek> | or they did, i haven't looked in several years |
21:44 | <snek> | i would assume they didn't delete that though lol |
21:45 | <snek> | the pattern is [][Symbol.iterator]().__proto__.__proto__ |
21:45 | <snek> | or getPrototypeOf instead |
21:47 | <snek> | so anyway to handle the case where they don't, we have this Iterator.from helper, and you can throw that in random places as needed |
21:47 | <snek> | but it exists purely to paper over the prototype |
21:48 | <snek> | like just thinking about the code i write for my day-to-day work, i would basically never use Iterator.from |
22:14 | <bakkot> |
i would guess well over 99% of code is internal, not in libraries, and in internal code you are not dealing with untrusted stuff, as a rule |
22:19 | <bakkot> | also I guess I am missing what the benefit of the internal slot is supposed to be |
22:28 | <rbuckton> | I voiced an opinion, several years ago now, that I believed the iterator helper methods should always just do this[Symbol.iterator]() so that they would work for both Iterator and Iterables. Then, the presence of Symbol.iterator would be enough of a brand check (insomuch as it is the same amount of brand checking that yield* and for..of care about). |
22:30 | <rbuckton> | most custom ones already inherit from Iterator.prototype { next() { ... }, [Symbol.iterator]() { return this; } } . |
22:40 | <bakkot> | rbuckton: why would you ever be invoking an iterator helper method on something which was an iterable-and-not-iterator? |
22:41 | <bakkot> | what is an example of some code where that might come up? |
22:51 | <rbuckton> | My original argument stems from my belief that Iterator is still the wrong level of abstraction. But aside from that, if the iterator helper methods were to only care about whether the object had a next() then we would have the same kind of duck typing we get with .then() on Promises. As a result, we end up needing a brand check of some kind which a custom iterator like { next() { ... } } wouldn't have. |
22:51 | <ljharb> | that wouldn't be any different than the current proposal which just checks the [[Prototype]] - it's not a brand check if it's based on a public property. |
22:52 | <ljharb> | Most custom "Iterators" I've seen are just Iterator.from first? |
22:52 | <rbuckton> | Honestly I wish we'd left IteratorPrototype alone and had adopted a chainable wrapper object over Iterable, or just bare functions to work with |> , but we're probably already too far gone. |
22:53 | <snek> | i definitely do not want to argue about iterable vs iterator more |
22:55 | <rbuckton> | i definitely do not want to argue about iterable vs iterator more Iterator.from() that could have been an Iterable.from() (or some other name) that works over Iterables really reinforces that belief. |
22:55 | <ljharb> | as has been said many times, iterable isn't a thing. We don't have Thenable.from either. |
22:56 | <ljharb> | it's an adjective to describe the presence of a method. |
22:56 | <rbuckton> | Iterable is a thing in the spec. Its an object with a [Symbol.iterator]() method. |
22:56 | <snek> | thats what an iterator is too |
22:56 | <ljharb> | iterators are kind of both, in that they're a thing and also a protocol |
22:56 | <snek> | its all a huge mess |
22:56 | <ljharb> | and yes, iterators are a mess |
22:56 | <ljharb> | Iterator.from always wrapping into a "Real" Iterator will help clean it up. |
22:57 | <ljharb> | (that built-in iterators all happen to be iterable isn't a part of the iterator contract, it's just a convenience these happen to have) |
22:57 | <rbuckton> | According to the spec, a "Real" iterator is an object with a next() method that returns a { value, done } object. |
22:57 | <rbuckton> | The fact that built-in iterators happen to have a shared prototype doesn't matter to that definition. |
22:57 | <rbuckton> | And the fact they have a shared prototype was always strange to me. |
22:57 | <snek> | js is indeed strange |
22:58 | <ljharb> | a real Promise is more than just a thenable. what i thought this proposal was doing was reifying a concept of a real iterator. |
22:59 | <rbuckton> | TBH, I will almost never reach for iterator helpers when I could use a more fully-capable third-party library, which is the only reason I haven't pushed back harder against it. |
23:00 | <rbuckton> | I don't think reifying Iterator will have anywhere near the impact that Promise did. At least, I certainly hope it doesn't. |
23:00 | <ljharb> | i think that it wouldn't on its own, but the iterator helpers themselves are why it will. |
23:01 | <ljharb> | i believe this proposal advancing will trigger a sea change in the way APIs are designed - everyone will start preferring lazy computation and passing around iterators instead of collections, which is a big shift from the current world where people tend to pass arrays. |
23:05 | <rbuckton> | i believe this proposal advancing will trigger a sea change in the way APIs are designed - everyone will start preferring lazy computation and passing around iterators instead of collections, which is a big shift from the current world where people tend to pass arrays. |
23:08 | <rbuckton> | I honestly think that if |> had advanced more quickly we wouldn't be pursuing this at all. |
23:08 | <snek> | i really wish we could add things to the stdlib without every delegate being involved in every single decision of every function |
23:09 | <snek> | temporal achieved this by being large and complex |
23:11 | <rbuckton> | i really wish we could add things to the stdlib without every delegate being involved in every single decision of every function |
23:11 | <snek> | everything is impactful in some way 🤷 |
23:12 | <snek> | and to be clear i didn't mean you specifically, i was just thinking about how this has been a proposal for like 3 years |
23:12 | <snek> | its depressing 😔 |
23:17 | <bakkot> | in fairness it's mostly just that people pick it up and put it down |
23:17 | <bakkot> | it's advancing now because michael and I found time to get back to it |
23:17 | <bakkot> | not so much because of there being an insurmountable number of details |
23:20 | <bakkot> | Iterator helper methods do only care about whether their receiver has a next method. But this is not at all the same kind of duck typing as with .then on promises, because it's on the receiver, not an arbitrary other thing; also nothing switches on its presence, just unconditionally attempts to call it. So it lacks the big two problems that thenables have, which are a.) consuming a string-named property on an argument, not just the receiver, and b.) changing behavior based on the presence or absence of the property instead of simply throwing in its absence |
23:23 | <bakkot> | on that note, ljharb should we maybe merge https://github.com/tc39/agendas/pull/1233 now, so delegates get a chance to review? we can always back it out if approval doesn't come through |
23:24 | <bakkot> | anyway, I do want to strongly affirm that iterator helpers are precisely the correct abstraction for what they are |
23:26 | <bakkot> | if the helpers consumed iterables they either a.) produce single-shot iterables, which would be weird and confusing or b.) produce reusable iterables, which would be weird and confusing for other reasons, i.e. https://stackoverflow.com/a/28513908 |
23:27 | <bakkot> | "iterator helpers" or some equivalent are a solution common to most mainstream languages - java, rust, scala, c++ (boost), scala - and they work well |
23:28 | <bakkot> | this is not uncharted territory here |
23:29 | <bakkot> | and I do expect them to get wide adoption, for the same reason they get wide adoption in other languages: they are the right abstraction to express what you want in a wide variety of scenarios, namely single-shot deferred computation |
23:45 | <rbuckton> | ES2015 established syntactic support for Iterable with for..of and yield* , even "one-shot" Iterables with generators. The only reason iterator helpers can be used with these constructs is that they are also "one-shot" Iterables. The languages you've referenced with a preference for iterator's are all statically typed. A function can receive an iterator and know its an iterator. |
23:47 | <rbuckton> | In JS, we are left with either "duck typing" or checking @@iterator. If @ljharb is correct, an ecosystem will be built around receiving iterators as well as producing them. And those functions may also need to check the input type. |
23:48 | <rbuckton> | Since the ecosystem likes to write overloads, they need some way to check if the input is an Iterator. next() is not sufficient. It is Promise.p.then() all over again, even if it's a smaller case. |
23:50 | <rbuckton> | Today the best way to do that overload check is to check for @@iterator, making the check for an Iterable. That at least still works for iterator helpers, but not for user defined { next() {}} . |
23:51 | <snek> | all iterators are also iterables |
23:51 | <snek> | i think checking for @@iterator is fine |
23:51 | <rbuckton> | Maybe a reified Iterator helps, but it is still just a "one-shot" Iterable as far as for-of , yield* , [...iter] , etc. are concerned. |
23:52 | <bakkot> | I do not expect there to be an ecosystem built around receiving iterators. why would there be? you can receive an iterable and consume it only once, and that works just as well. |
23:52 | <snek> | as for "passing iterators around" that would be interesting i guess, though i don't imagine it becoming that common |
23:52 | <snek> | looking at other languages like rust |
23:52 | <rbuckton> | all iterators are also iterables |
23:53 | <snek> | yes you're right, all the ones that inherit from the prototype |
23:53 | <rbuckton> | as for "passing iterators around" that would be interesting i guess, though i don't imagine it becoming that common |
23:53 | <snek> | yeah i don't agree with the prediction |
23:55 | <snek> | but either way i don't think it is too problematic |
23:57 | <ljharb> | if the only way to know if something is an iterator is to consume it, then everyone will have to Iterator.from(x).map(x=>x) to be sure they’re dealing with one. so why not build that into Iterator.from? |
23:57 | <bakkot> | the .map is not necessary |
23:57 | <bakkot> | it does nothing |