-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
nextLast() instead of next("last") #11
Comments
The word "next" appearing anywhere is the confusion, imo - "next" means forward, and "nextBack" is just as confusing. |
@ljharb Do u have any suggestion of the api name? |
|
let [a, ...b, c] = noDeiterSupportIteratorOf(0, 1, 2, 3)
// ^0th rest^ ^1th
a // 0
c // 1 (!! we call next("last") but "last" is ignored by the next method)
b // 2, 3 but have no idea about what name we should use. |
That’s true - but double-ended without being bidirectional is also a confusing concept. It’s more like two separate iterators on the same underlying collection - if it’s one iterator, then it probably shouldn’t have multiple directions/ways to iterate? |
@Jack-Works Yes, we always need a mechanism to mark deiters. This was discussed in #5. For As such solution,
If we move to |
@ljharb , bidirectional means u can restore to previous state, double-ended means u can consume in both ends, I think the problem is not they are confusing, but they are relative abstract interfaces/concepts, unfamiliar to the js programmers.
Yeah that's why it is called "DOUBLE-ended" iterator. Normally the underlying collection (or similar structure, for example, Of coz we can have two separate iterators. That's reverse iterator . But reverse iterator is not enough for The good news is only library authors need to care about iterator, most programmers just use destructuring and never need to worry about them. |
@hax You never really addressed my original complaint. Let's say this proposal proceeds. I write this: function map(iterator, fn) {
return {
next() {
return fn(iterator.next());
},
nextLast() {
return fn(iterator.nextLast());
}
}
} How can I tell if To avoid this problem I'd need to write function map(iterator, fn) {
const next = () => {
return fn(iterator.next());
};
return iter.nextLast ? { next, nextLast } : { next };
} This rules out using generators to implement iterator helpers, so for starters you've made pretty much all current iterator code unsafe. |
You don’t need any iterator protocol changes to support |
But the intention of this proposal is to not exhaust the iterator. |
That’s very confusing to me; any use of … should exhaust the iterator, since it’s explicitly saying “grab everything available” |
Not necessary, do you remember we introduce |
I can't get over taking what seems to be a relatively niche use and fundamentally altering the definition of what an iterator is to support it! It seems to me like you've allowed the highest level concerns to drive the lowest level concerns. You say that it should be possible to write |
@Jack-Works thats a fair point :-P any use of it with a variable, then. How else would you fill it with the proper values? @conartist6 not sure who is “your” here - are you talking about the proposal or about my claim that it should be achievable without changing iterators? |
@ljharb Exactly. Since capturing Let me see if I can be even more clear about what I mean. For example, your algorithm for If we say that everyone who writes an iterator is also writing part of the internal implementation of destructuring, then we must also assume that some of those implementations will be incorrect. Sometimes the author will forget to stop the forward iterator when it hits the back cursor, and since this is javascript someone will be relying on the nonsense behavior and it will never be possible to remove, and this would become one of those language features that we just tell people not to use if they value their sanity... |
I should note that I'm not unbiased (though I hope my arguments are logical). But yeah, I have my own version of this proposal: https://es.discourse.group/t/array-prototype-reversed-map-set/556 |
Sorry, to clarify I meant "you" as "the authors of this proposal" except where I tagged Jordan. I am aware that a buffering solution is possible, and in fact I've implemented that solution as part of |
function map(upstream, fn) {
let iter = { __proto__: Iterator.prototype }
if (upstream.next) iter.next = () => fn(upstream.next())
if (upstream.nextLast) iter.nextLast = () => fn(upstream.nextLast())
} Of coz this code is very naive (not passing the args of function *map(upstream, fn) {
for (let v of upstream) yield fn(v)
} Such code will invoke
No. All current iterators still work, they just don't support About generator, I also hope we could allow people write deiter via generator syntax. But even without generator, at least most built-in iterators and iterator helpers could support double-ended, so still have enough benefit. Even in current situation, you still could write deiter via generator: class IntRange {
constructor(start, end) {
if (!Number.isSafeInteger(start)) throw new TypeError()
if (!Number.isSafeInteger(end)) throw new TypeError()
this.start = start
this.end = end
}
@doubleEnded *[Symbol.iterator]() {
let {start, end} = this
let method = yield
while (start < end) {
method = method == "nextLast"
? yield --end
: yield start++
}
}
} The function doubleEnded(g) {
const {next} = g.prototype
g.prototype.start = function () {
if (this.started) return
this.started = true
Reflect.apply(next, this, [])
}
g.prototype.next = function (v) {
this.start()
return Reflect.apply(next, this, ['next'])
}
g.prototype.nextLast = function () {
this.start()
return Reflect.apply(next, this, ['nextLast'])
}
} |
See https://github.com/tc39/proposal-deiter/blob/main/why-deiter.md . It's better to open a separate issue to discuss the design choice. |
Only if they want to support double-ended semantic. If not (for example, stream are normally one-way), they don't need to care about deiter. And in such cases,
Double-ended is not easy, but not as hard as you described. The really hard part is iterator transformation, and it's already hard even without double-ended semantics, different user-land iterator transformations libraries have many subtle semantic differences. This is why we should have a standardardized built-in iterator helpers. I don't think average js programmers should do it themselves, so I would agree to tell people do not write "iterator transformation" themselves. For library authors, it's their duty to solve relative hard problem or there is no need to use library. 😅 |
@hax the "why-deiter" doesn't talk about a method C, which is "support all iterables, but cache yielded results until the iterator is exhausted". |
@ljharb that would be sound to implement, but it doesn't offer any way to do things like get the entries of a |
@conartist6 sure, but the solution to that is the same as "but the default iterator doesn't provide just keys, just values, or just entries" - make a new method that produces the iterator you want, like |
I'd be strongly in favor of the combination of |
@hax I disagree with you that it would be reasonable for tools to define different methods on their outputs depending on their inputs. That is something I always explicitly try to avoid for these reasons:
|
@ljharb I don't see any difference between "Mechanism A: Based on Array" with "method C". |
@conartist6 What's the precise semantic of "the combination of |
@hax because it would never be based on arrays, because it's iterable destructuring - so it should work for all iterables, using Lists, and only reify to an array as the final step. |
@conartist6 All these reasons apply to all similar things which have subtype/supertype, for example readonly collections. I agree you we need to care about all these aspects, but we need balance the different use cases, and in deiter and readonly collections cases, I think they all have enough benefit to add to the language.
Actually this is why type system are useful.
We write types not because it's easy to write, (many js idioms require very complex type, very hard to write, actually compare to many "type gymnastics", typing deiter is "easy",) but because it could help us to maintain the code and help others to use our code.
We need engine people to estimate the cost, but as I understand, introducing deiter won't harm to engine efficiency. On the contrary, buffer-based mechanisms add burden to the engine to optimize the builtins (if people use double-ended destructuring broadly, engines will eventually be pushed to optimize them) and eventually engines would have to implement deiter-like things, but that will become builtins-only magic, and like many magics today, if userland code involves, u suddenly lost the magic and got perf cliff. |
@ljharb It works for all iterables. I'm sorry if "based on array" sound different, but here "based on array" only mean the semantic is close to the traditional array-based operations (get rest, and Of coz the implementation could (and should) use List or something else and only reify to an array if (rest is) needed. |
You're saying that there all these ways that we can deal with the burden of supporting this pattern. I'm saying I haven't yet seen any evidence of a use case important enough to be worth the extra complexity added to the language and the ecosystem. |
@conartist6 In general, I think if we want to support double-ended destructuring, the best solution is deiter, buffered solution have the issue of perf, and likely to get builtin-only optimization which cause the worse ecosystem issue. I've explained that in the why-iter doc. And note if we adopt buffered solution it make deiter never possible in the language. Even userland implementation could cause similar ecosystem issue if widely used. I hope we can discuss more about the consequence of different possible mechanisms of double-ended destructuring, if u like. But maybe it should in a separate issue. |
Even the example I've been using isn't really right. I keep writing out Your "why deiter" document doesn't really explain what all this is useful for, it just assumes that you want to do the things it allows you to do! |
Not only
I think I already said that, it explained "why deiter" (is the best solution to support double-ended destructuring and why buffered solution is not good, and there is no other possible solution as I understand.) I'm ok if anybody think the doc is not correct and we can discuss it (just like we do here) but saying "it just assumes that you want to do the things it allows you to do" is not very helpful. |
from tc39/proposal-iterator-helpers#123 (comment) by @michaelficarra
I find this comment also need to consider, |
You talk about solutions, but you do not explain what real problems they solve. I understand wanting to be able to write I also still don't see the purpose of meet-in-the-middle cursors. They make it possible to write |
@conartist6 If you ask why add To be clear, if the committee think such syntax is not useful, the proposal will not advance. "Why deiter" only discuss the possible solutions to support that syntax. Deiter VS. Reverse iterator Reverse Iterator could be used to support The real main differences between reverse iterator and deiter are: Reverse iterator require you write two iterators, need to maintain the iteration algorithm correctness in two place, deiter only need you write one iterator. About "meet-in-the-middle" complexity Deiter does not necessarily based on "meet-in-the-middle cursors", though "meet-in-the-middle cursors" is a common pattern for deiter/deque. It seems you claim that it's too complex to most programmers. I already respond that in previous comment, and there is one more thing, if it's a common pattern, then it's easily to have a common helpers to deal with the complexity. For example, you only need write the deiter for |
* nextLast() instead of next("last") fix #11 * fixup: [spec] `npm run build` * miss next("last") * another miss Co-authored-by: hax <[email protected]>
As iterator helpers proposal decide to not pass the protocol, we have to switch to |
Yes. Your solution also requires iterables to create new reverse iteration logic in order for it to be possible to use
I disagree wholeheartedly. Only deiter requires a new definition of
It's funny, you and I see this exactly opposite. You say that technically a deiter as defined by this proposal is is "one iterator" which must be simpler than two iterators. I disagree completely. You haven't eliminated the need to write the second iterator, you've just ensured that the logic for both iterators has to share state and be intertwined. |
Double-ended iterators do not need seperate reverse iteration logic (just call
Consider Of coz u could write
I didn't say one deiter must be simpler than two iterators. Deiter could be a little bit complex, especially in the most simple case, "meet-in-the-middle" could be a marked logic. But if it's a simple case, this won't add too much complexity. You already see some example codes in my previous comments, and hope u could measure the complexity of such code in objective manner. In the complex case, for example, writing a deiter for tree traversal, the complexity mainly from the data structure, so "meet-in-the-middle" won't be a big deal. The total complexity of deiter and two iterator might be similar, but with deiter you get many other benefit, like maintaining iteration logic in one place not two, and supporting
I don't object anyone think a double-ended iterator as two interwined iterators, if it could help them to understand. Just like double-headed eagle might be have the source of two eagles:
|
We both want two types of iterator. You want single and double ended. I want forwards and backwards. Does that make me more Greek or Hittite? |
In the plenary which this proposal was first presented, it was suggested (by @codehag if I recall correctly) to use
nextBack()
instead ofnext("back")
. As #9 / #10 changed "back" to "last", it will benextLast()
instead ofnext("last")
.The reason I didn't follow Rust
nextBack()
API in the first place, is because the consideration of generators (#9 (comment)) . But I'm reconsidering it.Several points:
nextLast()
looks even more consistent withfindLast
,lastIndexOf
and potential iterator helperstakeLast
,dropLast
, etc.nextLast
could be used to test whether an iterator is a double-ended iterator (or support reverse iteration), so we could drop separate symbols@@doubleEndedIterator
(or@@reverseIterator
), and also do not need separate flag fields (whichnext("last")
may need). This seems the simplest solution of How to mark double-ended iterator #5 .next("last")
could allow writing deiter in generators withfunction.sent
proposal. Unfortunately,function.sent
proposal have several issues, I try to reshape the syntax from meta propertyfunction.sent
to "receive param", but as previous meeting, we are still far from consensus. So I'd like to drop the initial idea of "direct support writing deiter in generators", focus on the double-ended iterators itself, leave the generator issue tofunction.sent
or future proposals (nextLast()
could still be layered onnext("last")
in such proposals).The text was updated successfully, but these errors were encountered: