├── .npmrc ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── package.json ├── double-ended-helper.js ├── spec.emu ├── .gitignore ├── LICENSE ├── destructuring.md ├── iterator-helpers.md ├── why-deiter.md ├── README.md └── index.html /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html -diff merge=ours 2 | spec.js -diff merge=ours 3 | spec.css -diff merge=ours 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Deploy spec 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | - run: npm install 15 | - run: npm run build 16 | - name: commit changes 17 | uses: elstudio/actions-js-build/commit@v3 18 | with: 19 | commitMessage: "fixup: [spec] `npm run build`" 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "template-for-proposals", 4 | "description": "A repository template for ECMAScript proposals.", 5 | "scripts": { 6 | "start": "npm run build-loose -- --watch", 7 | "build": "npm run build-loose -- --strict", 8 | "build-loose": "ecmarkup --verbose spec.emu index.html" 9 | }, 10 | "homepage": "https://github.com/tc39/template-for-proposals#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tc39/template-for-proposals.git" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "ecmarkup": "^4.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /double-ended-helper.js: -------------------------------------------------------------------------------- 1 | // May be standardized to Iterator.doubleEnded() in the future 2 | // See https://github.com/tc39/proposal-deiter#generator for usage 3 | function doubleEnded(g) { 4 | return function (...args) { 5 | const context = {} 6 | args.push(context) 7 | const iter = Reflect.apply(g, this, args) 8 | return { 9 | __proto__: IteratorPrototype, 10 | next() { 11 | context.method = "next" 12 | return iter.next() 13 | }, 14 | nextLast() { 15 | context.method = "nextLast" 16 | return iter.next() 17 | }, 18 | return(v) { return iter.return(v) }, 19 | } 20 | } 21 | } 22 | const IteratorPrototype = function *() {}.prototype.__proto__.__proto__ 23 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
 7 | title: Proposal Title Goes Here
 8 | stage: -1
 9 | contributors: Your Name(s) Here
10 | 
11 | 12 | 13 |

This is an emu-clause

14 |

This is an algorithm:

15 | 16 | 1. Let _proposal_ be *undefined*. 17 | 1. If IsAccepted(_proposal_), 18 | 1. Let _stage_ be *0*. 19 | 1. Else, 20 | 1. Let _stage_ be *-1*. 21 | 1. Return ? ToString(_proposal_). 22 | 23 |
24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | pnpm-lock.yaml 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ECMA TC39 and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /destructuring.md: -------------------------------------------------------------------------------- 1 | # Semantic details of double-ended destructuring 2 | 3 | This is a preliminary document to define the semantic details of all edge cases 4 | 5 | ## Destructuring result if no enough item 6 | 7 | ```js 8 | let [...a, b] = [1] 9 | a // [] 10 | b // 1 11 | ``` 12 | 13 | --- 14 | 15 | ```js 16 | let [...a, b] = [] 17 | a // [] 18 | b // undefined 19 | ``` 20 | 21 | --- 22 | 23 | ```js 24 | let [...a, b = 10] = [1] 25 | a // [] 26 | b // 1 27 | ``` 28 | 29 | Someone may expect `a` be `[1]` and `b === 10` at first glance, but if that way, `a` will always gather all items and `b` will always `10` so the syntax become useless. 30 | 31 | --- 32 | 33 | ```js 34 | let [a, ...b, c] = [1] 35 | a // 1 36 | b // [] 37 | c // undefined 38 | ``` 39 | 40 | --- 41 | 42 | ```js 43 | let [a, b, c, ...d, e] = [1, 2] 44 | a // 1 45 | b // 2 46 | c // undefined 47 | d // [] 48 | e // undefined 49 | ``` 50 | 51 | Always first fill the left side of `...`. 52 | 53 | --- 54 | 55 | ```js 56 | let [...a, b, c] = [1] 57 | a // [] 58 | b // 1 59 | c // undefined 60 | ``` 61 | 62 | Someone may expect `b === undefined && c === 1`. Both could be reasonable, currently I choose to follow Ruby and CoffeeScript, but maybe we should choose another side especially if consider execution order issue. 63 | 64 | Note, u always could use `let [...[...a, b], c] = [1]` to get `b === undefined && c === 1`. 65 | 66 | 67 | ## Execution order 68 | 69 | ```js 70 | let [a = console.log(1), ...[a = console.log(2)], b = console.log(3), c = console.log(4)] = [] 71 | ``` 72 | 73 | Should the order be 1,4,3,2 ? or 1,2,3,4 ? 74 | 75 | ```js 76 | let [a, ...b, c, d, e] = { 77 | *[Symbol.deIterator]() { 78 | yield 1 79 | yield 10 80 | yield 9 81 | throw new Error() 82 | } 83 | } 84 | ``` 85 | 86 | Should the result of c,d,e be all `undefined` or `9,10,undefined`? 87 | -------------------------------------------------------------------------------- /iterator-helpers.md: -------------------------------------------------------------------------------- 1 | Some iterator helpers already limit the reversibility. eg. `take(n)`, actually it means take the first n elements, so it already one-direction and no way to reverse. 2 | 3 | Or we could make iterators always reversible (means u could always `reverse()` it, `reverse()` just reverse the invoking of `next()` and `nextLast()`), because it just change the end which could be iterated. 4 | 5 | There are three type of iterables as double-ended iteration: 6 | 7 | 1. double-ended, which its iterator support both `next()` and `nextLast()` 8 | 2. forward-only, which its iterator only support `next()`, `reverse()` it will transform it to backward-only 9 | 3. backward-only, which its iterator only support `nextLast()`, `reverse()` it will transform it to forward-only 10 | 11 | Here are the simple result table for iterator helpers (note, most aggregator methods like `toArray`, `every`, etc. are direction-neutral so not listed here) : 12 | 13 | method \ upstream | double-ended | forward-only | backward-only 14 | -|-|-|- 15 | `forEach` | ✅ double-ended | ✅ forward-only | ✅ backward-only 16 | `map` | ✅ double-ended | ✅ forward-only | ✅ backward-only 17 | `filter` | ✅ double-ended | ✅ forward-only | ✅ backward-only 18 | `flatMap` | ✅ double-ended | ✅ forward-only | ✅ backward-only 19 | `reverse` | ✅ double-ended | ✅ backward-only | ✅ forward-only 20 | `indexed` | ✅ forward-only | ✅ forward-only | ❌ 21 | `take` | ✅ forward-only | ✅ forward-only | ❌ 22 | `drop` | ✅ double-ended | ✅ forward-only | ❌ 23 | `find` | ✅ | ✅ | ❌ 24 | `reduce` | ✅ | ✅ | ❌ 25 | `takeLast` | ✅ backward-only | ❌ | ✅ backward-only 26 | `dropLast` | ✅ double-ended | ❌ | ✅ backward-only 27 | `findLast` | ✅ | ❌ | ✅ 28 | `reduceRight` | ✅ | ❌ | ✅ 29 | 30 | Note: How to read the table: `upstream.method() => tablecell`, for example, `doubleEnded.map() => doubleEnded`, `forwardOnly.reverse() => backwardOnly`, `forwardOnly.takeLast() => ❌` (❌ means throw TypeError). 31 | 32 | Instead of throwing TypeError for ❌ , another option is return a "dead" iterator (which have no `next()` and `nextLast()`), though it seems useless. 33 | 34 | Maybe `take`, `takeLast` could keep double-ended, see https://github.com/tc39/proposal-deiter/issues/13 35 | 36 | About `flatMap`: need more investigation. 37 | 38 | -------------------------------------------------------------------------------- /why-deiter.md: -------------------------------------------------------------------------------- 1 | # Optional Mechanisms for Double-ended Destructructing 2 | 3 | ## Recap 4 | 5 | ```javascript 6 | let [first, ...rest, last] = [1, 2, 3, 4] 7 | first // 1 8 | last // 4 9 | rest // [2, 3] 10 | 11 | // rest can be omitted if not needed 12 | let [first, ..., last] = [1, 2, 3, 4] 13 | ``` 14 | 15 | In the [TC39 meeting in Sept 2020](https://github.com/tc39/notes/blob/master/meetings/2020-09/sept-24.md#double-ended-iterator-and-destructuring-for-stage-1), double-ended iterator and destructuring entered stage 1. Delegates agreed to extend the syntax of destructuring to support double-ended destructuring, but some delegates questioned whether double-ended iterator is needed as the underlying mechanism of double-ended destructuring. 16 | 17 | In order to better explore this issue, I will elaborate and analyze possible underlying mechanisms below. 18 | 19 | ## index-based (only supports array) 20 | 21 | The destructuring in Ruby and Rust only supports arrays, and the underlying mechanism is based on index access. To destructure iterable, you must first convert iterable to an array. 22 | 23 | Since the destructuring in JS is designed to be based on the Iterable protocol, not based on index access, and I believe the committee will not be willing to change this, this option is almost impossible to apply to JS. 24 | 25 | ## Mechanism A: Based on Array 26 | 27 | Under this mechanism, the semantics of the code `let [first, ...rest, last] = iterable` is basically: 28 | 29 | ```javascript 30 | let [first, ...rest] = iterable 31 | let [last] = rest.splice(-1, 1) 32 | ``` 33 | 34 | The advantage of this mechanism is simplicity. But there is a scale problem. 35 | 36 | The scale problem is that `iterable` may be a very long list, and a common use case is to take only the last few elements: 37 | 38 | ```javascript 39 | let iterable = BigInt.range(1n, 1_000_000n) 40 | 41 | let [a, b, ..., c, d] = iterable 42 | ``` 43 | 44 | In this case, we waste a lot of cpu power and memory, and the performance is poor by default. 45 | 46 | The engine can optimize the built-in collection types to alleviate it. However, this optimization only applies to the built-in iterators, the user-land iterators/generators still suffer it. 47 | 48 | From an engineering point of view, such optimization will eventually encourage some developers to write non-scale code, making the relevant code unable to have a definite performance expectation. 49 | 50 | For situations where performance is sensitive and deterministic performance expectations are required, developers must abandon the iterable protocol and destructuring syntax, use index access, or implement APIs like index access or similar double-ended iterator protocols by themselves. 51 | 52 | ```javascript 53 | // Assume range have API similar to index access 54 | let [a, b] = iterable 55 | let c = iterable.getItem(range.length - 2) 56 | let d = iterable.getItem(range.length - 1) 57 | ``` 58 | 59 | ```javascript 60 | // Assume range have API similar to double-ended iterator 61 | let [a, b] = iterable 62 | let [c, d] = iterable.lastItems(2) 63 | ``` 64 | 65 | Note: The above code does not handle the situation where the actual length of `iterable` is less than 4. If you need to ensure consistency with iterable/destructuring semantics (to avoid getting duplicate elements from both sides), the code will become quite complicated. 66 | 67 | > Mark Miller: ...the destructuring patterns and parameter patterns are not used at the scale where scaling is relevant. 68 | 69 | MM's comment at the last meeting was that if scale is needed, destructuring are not used. 70 | 71 | I think this is a "should be" or "as it is" problem. Regarding mechanism A, I agree that when scale is needed, destructuring should not be used. But did the engineer actually do this? 72 | 73 | In particular, code `let [first] = iterable` naturally has no scale problem (because it does not consume the entire iteration), but its corresponding `let [..., last] = iterable` has a scale problem. 74 | 75 | In this way, although the introduction of double-ended destructuring increases the ergonomics , it also introduces an additional mental burden. 76 | 77 | Developers at least need to determine their performance requirements before using double-ended destructuring (even according to Mark Miller's opinion, this must be the case). However, in many cases, we are not clear on performance requirements at the beginning (junior engineers often lack this ability), or as the project involve, performance requirements will change. Many experienced engineers tend to ensure that the code always has deterministic performance expectations, even if there is no clear performance requirement at the beginning. 78 | 79 | From my engineering experience, I think this will make the double-ended destructuring into a [鸡肋 (chicken rib, things of little value or interest, yet pitiable if given up or thrown away)](https://en.wiktionary.org/wiki/%E9%B8%A1%E8%82%8B). 80 | 81 | ## Mechanism B: Based on double-ended iterator 82 | 83 | Under this mechanism, the semantics of the code `let [first, ...rest, last] = iterable` is basically: 84 | 85 | ```javascript 86 | let iterator = Iterator.from(iterable) 87 | let first = iterator.next().value 88 | let last = iterator.next('back').value 89 | let rest = iterator.toArray() 90 | // iterator.return?.() // if rest is omitted 91 | ``` 92 | 93 | It seems more complicated than mechanism A, but this is because when explaining mechanism A, the underlying iterator semantics is not expanded. If expanded, it is basically: 94 | 95 | ```javascript 96 | let iterator = Iterator.from(iterable) 97 | let first = iterator.next().value 98 | let rest = iterator.toArray() 99 | let last = rest.splice(-1, 1)[0] 100 | ``` 101 | 102 | As this version, the semantic complexity of the two is similar. 103 | 104 | Considering the possible optimization of built-in iterable types by the JS engines under mechanism A, it is essentially an abstraction similar to double-ended iteration (especially for types without index access such as `Set` and `Map` ). From the point of view of the object protocols, the interface capability of an object increases from normal iterable to double-ended iterable to indexable. Therefore, instead of confining this mechanism to the engine, it is better to generalize the protocol at the language level, so that engineers (especially the authors of the libraries) can implement the protocol by themselves, provide a performance-friendly implementation, and give full power to the double-ended destructuring syntax. 105 | 106 | If the object does not implement double-ended iterable protocol, using double-ended destructuring will throw a TypeError, which ensures the deterministic performance expectations of using double-ended iterators and double-ended destructuring. Note that for performance-insensitive cases, developers can always first convert an iterable to an array to use double-ended destructuring. Even if there is a possibility of abusing, because of the explicit conversion code, it's easy for tools and code review to discover them. 107 | 108 | Note: Further, `iterator.toArray()` in iterator helpers proposal can also be extended to allow implementers of iterators to provide a performance-friendly version. 109 | 110 | ```javascript 111 | Iterator.prototype.toArray = function toArray() { 112 | let {done, value} = this.next('rest') 113 | if (done) { 114 | if (value === undefined) return [] 115 | if (Array.isArray(value)) return value 116 | throw new TypeError() 117 | } 118 | 119 | let a = [value] 120 | for (;;) { 121 | let {done, value} = this.next() 122 | if (done) return a 123 | a.push(value) 124 | } 125 | } 126 | ``` 127 | 128 | The possible cost of introducing double-ended iterator is the addition of the concept of "double-ended iterable/iterator" and the need to update the implementation of built-in iterators (most built-in iterators should support double-ended iteration). 129 | 130 | We can compare it to the [reverse iterator](https://github.com/tc39/proposal-reverseIterator) proposal, which adds the concept of "reverse iterator" and also needs to add reverse iteration capabilities to the built-in collection (most built-in collections should support reverse iteration). 131 | 132 | Double-ended iterator actually covers all use cases of reverse iterators and related discussions, and is a more general mechanism. So in terms of the total cost of the two proposals, the cost of double-ended iterator may be smaller. 133 | 134 | ## Summary 135 | 136 | Mechanism A (based on arrays) and mechanism B (based on double-ended iterator) have similar semantic complexity. 137 | 138 | Mechanism A does not need to introduce new concept; mechanism B introduces the concept of "whether the double-ended iterator protocol is implemented". 139 | 140 | Mechanism A does not need to update the implementation of the built-in iterators; mechanism B needs to update the built-in iterators to implement the double-ended iterator protocol. But under mechanism A, if the engine optimizes its performance, it is actually very similar to implementing a double-ended iterators. 141 | 142 | Mechanism A has a scale problem; Mechanism B is performance-friendly and does not have the scale problem. 143 | 144 | Mechanism A is only limited to destructuring; mechanism B is a more general mechanism that can solve both destructuring and reverse iterator use cases together. 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Double-Ended Iterator and Destructuring 2 | 3 | Stauts: Stage 1 4 | 5 | Author: HE Shi-Jun (hax) 6 | 7 | Champion: HE Shi-Jun (hax) 8 | 9 | - [Advanced to Stage 1 on Sept 2020](https://github.com/tc39/notes/blob/main/meetings/2020-09/sept-24.md#double-ended-iterator-and-destructuring-for-stage-1) 10 | - [Incubator call on Dec 3, 2020](https://github.com/tc39/incubator-agendas/blob/main/notes/2020/12-03.md) 11 | - [Stage 0 -> 1 (outdated) presentation](https://johnhax.net/2020/tc39-sept-deiter/slide) 12 | - [Stage 1 update on July 21, 2022](https://johnhax.net/2022/deiter/slide) 13 | 14 | ## Motivation 15 | 16 | Python and Ruby support `(first, *rest, last) = [1, 2, 3, 4]`, CoffeeScript supports `[first, rest..., last] = [1, 2, 3, 4]`, and Rust supports `[first, rest @ .., last] = [1, 2, 3, 4]`, all resulting in `first` be `1`, `last` be `4`, and `rest` be `[2, 3]`. But [surprisingly](https://stackoverflow.com/questions/33064377/destructuring-to-get-the-last-element-of-an-array-in-es6) `[first, ...rest, last] = [1, 2, 3, 4]` doesn't work in JavaScript. 17 | 18 | And in some cases we really want to get the items from the end, for example getting `matchIndex` from [String.prototype.replace when using a function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter): 19 | 20 | ```js 21 | string.replace(pattern, (fullMatch, ...submatches, matchIndex, fullString) => { 22 | // `matchIndex` is always the second to last param (the full string is the last param). 23 | // There may be many submatch params, depending on the pattern. 24 | }) 25 | ``` 26 | 27 | ## Solution 28 | 29 | A naive solution is making `let [first, ...rest, last] = iterable` work as 30 | 31 | ```js 32 | let [first, ...rest] = iterable 33 | let last = rest.pop() 34 | ``` 35 | 36 | The concern is it requires saving all items in the `rest` array, although you may only need `last`. A possible mitigation is supporting `[..., last] = iterable` which saves the memory of `rest`, but you still need to consume the entire iterator. In the cases where `iterable` is a large array or something like `Number.range(1, 100000)`, it's very inefficient. And in case like `let [first, ..., last] = repeat(10)` which `repeat` is a generator returns infinite sequence of a same value, theoretically both `first` and `last` could be `10`, but you just get a dead loop. 37 | 38 | Instead of the naive solution, we introduce the double-ended iterator (like Rust std::iter::DoubleEndedIterator). A double-ended iterator could be consumed from both ends, `next()` consume the first item from the rest items of the sequence, `nextLast()` consume the last item from the rest items of the sequence. 39 | 40 | ```js 41 | let a = [1, 2, 3, 4, 5, 6] 42 | let deiter = a.values() // suppose values() would be upgraded to return a double-ended iterator 43 | deiter.next() // {value: 1} 44 | deiter.next() // {value: 2} 45 | deiter.nextLast() // {value: 6} 46 | deiter.next() // {value: 3} 47 | deiter.nextLast() // {value: 5} 48 | deiter.nextLast() // {value: 4} 49 | deiter.nextLast() // {done: true} 50 | deiter.next() // {done: true} 51 | ``` 52 | 53 | With double-ended iterators, `let [a, b, ..., c, d] = iterable` would roughly work as 54 | 55 | ```js 56 | let iter = iterable[Symbol.iterator]() 57 | let a = iter.next().value 58 | let b = iter.next().value 59 | let d = iter.nextLast().value 60 | let c = iter.nextLast().value 61 | iter.return() 62 | ``` 63 | 64 | ## Generator 65 | 66 | Generator functions provide a concise syntax to create iterators in the userland. For example, you can write `values(arrayLike)` which returns iterator for all array-likes: 67 | 68 | ```js 69 | function *values(arrayLike) { 70 | let i = 0 71 | while (i < arrayLike.length) { 72 | yield arrayLike[i] 73 | i++ 74 | } 75 | } 76 | ``` 77 | 78 | To implement double-ended version of `values(arrayLike)` in userland, we could use the [`doubleEnded` helper](double-ended-helper.js): 79 | 80 | ```js 81 | const values = doubleEnded(function *values(arrayLike, context) { 82 | let i = 0, j = 0 83 | while (i + j < arrayLike.length) { 84 | if (context.method == "nextLast") { 85 | yield arrayLike[arrayLike.length - 1 - j] 86 | j++ 87 | } else { // context.method == "next" 88 | yield arrayLike[i] 89 | i++ 90 | } 91 | } 92 | }) 93 | ``` 94 | It could also be used as decorator (stage 3 proposal): 95 | ```js 96 | class IntRange { 97 | constructor(start, end) { 98 | if (!Number.isSafeInteger(start)) throw new TypeError() 99 | if (!Number.isSafeInteger(end)) throw new TypeError() 100 | this.start = start; this.end = end; Object.freeze(this) 101 | } 102 | @doubleEnded *[Symbol.iterator](context) { 103 | let {start, end} = this 104 | while (start < end) { 105 | if (context.method == "nextLast") yield --end 106 | else yield start++ 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ## Iterator helpers and reverse iterator 113 | 114 | [Iterator helpers](https://github.com/tc39/proposal-iterator-helpers) add some useful methods to iterators and async iterators. Most methods are easy to upgrade to support double-ended. For example, `map()` could be implemented like: 115 | 116 | ```js 117 | // only for demonstration, the real implementation should use internal slots 118 | Iterator.prototype.map = function (fn) { 119 | let iter = { 120 | __proto__: Iterator.prototype, 121 | return(value) { this.return?.(); return {done: true, value} } 122 | } 123 | if (this.next) iter.next = () => fn(this.next()) 124 | if (this.nextLast) iter.nextLast = () => fn(this.nextLast()) 125 | return iter 126 | } 127 | // usage 128 | let [first, ..., last] = [1, 2, 3].values().map(x => x * 2) 129 | first // 2 130 | last // 6 131 | ``` 132 | 133 | Furthermore, some extra iterator helpers like `toReversed`, `takeLast`, `dropLast` and `reduceRight`, etc. could be introduced. 134 | 135 | For example, `toReversed()`: 136 | 137 | ```js 138 | // only for demonstration, the real implementation should use internal slots 139 | Iterator.prototype.toReversed = function () { 140 | let next = this.nextLast?.bind(this) 141 | let nextLast = this.next?.bind(this) 142 | let ret = this.return?.bind(this) 143 | return { 144 | __proto__: Iterator.prototype, 145 | next, nextLast, return: ret, 146 | } 147 | } 148 | // usage 149 | let a = [1, 2, 3, 4] 150 | let [first, ..., last] = a.values().toReversed() 151 | first // 4 152 | last // 1 153 | ``` 154 | 155 | With double-ended iterators and `toReversed()` helpers, we may not need [reverse iterator](https://github.com/tc39/proposal-reverseIterator). Even we still want reverse iterator as a separate protocol, we could also easily have a default implementation for it. 156 | 157 | ```js 158 | Iterator.prototype[Symbol.reverseIterator] = function () { 159 | return this[Symbol.iterator].toReversed() 160 | } 161 | ``` 162 | 163 | ## FAQ 164 | 165 | ### I like the idea of allowing `...` in the middle of the destructuring pattern, but why introduce "double-ended" iterator? 166 | 167 | Because JavaScript `[first, ...rest] = sequence` destructuring is based on iterable protocol, so we should make `[first, ...rest, last] = sequence` also based on iterable protocol. And `[a, b, ...rest, c, d, e] = sequence` could be perfectly interpreted as "take first two elements from the sequence, then take last three, and the rest", aka. allowing take elements from the the other end of a sequence, which conceptually same as what "double-ended" mean in the common data structure [deque](https://en.wikipedia.org/wiki/Double-ended_queue). Note JavaScript already have some Array APIs behave as double-ended: 'indexOf/lastIndexOf, reduce/reduceRight, find/findLast', etc. So generalizing the concept could increase the consistency of all APIs (include user-land libraries) which may based on similar abstraction. See [Optional Mechanisms for Double-ended Destructructing](https://github.com/tc39/proposal-deiter/blob/main/why-deiter.md) for further analysis about the consideration of performance, mental burden and design cost. 168 | 169 | ### How could a iterator/generator move back to the previous status? 170 | 171 | It's not "move back" or "step back", it's "consume the next value from the other end" or "shorten range of values from the other end". 172 | 173 | There are two concepts easy to confuse, _bidirectional_ vs. _double-ended_. Bidirectional means you can invoke `next()` (move forward) or `previous()` (move backward). Double-ended means you can invoke `next()` (consume the first item from the rest items of the sequence) or `nextLast()` (consume the last item from the rest items of the sequence). 174 | 175 | The initial version of this proposal used `next("back")` which follow Rust `nextBack()`. The term "back" may come from C++ vector/deque (see https://cplusplus.com/reference/vector/vector/back/), means "last element". This term usage is not popular in JavaScript ecosystem and cause confusion, so we changed the word from "back" to "last". 176 | 177 | ### What is "double-ended", how it differ to "bidirectional"? 178 | 179 | To help understand the concepts, you could imagine you use cursors point to positions of a sequence and get value at the position. Normal iteration need only one cursor, and initally the cursor is at the most left side of the sequence. You are only allowed to move the cursor to right direction and get the value of the position via `next()`. Bidrectional means you could also move the cursor to left direction via `previous()`, so go back to the previous position of the sequence, and get the value (again) at the position. 180 | 181 | Double-ended means you have **two** cursors and initally one is at the most left side and can only move to right direction, the other is at most right side and can only move to left direction. So you use `next()` move the first cursor to right and get the value at its position, use `nextLast()` move the second cursor to left and get the value at its position. If two cursors meet the same postion, the sequence is totally consumed. 182 | 183 | You could find these two concept are actually orthogonal, so theorcially we could have both bidirectional and double-ended. So `next()`/`previous()` move the first cursor right/left, `nextLast()`/`previousLast()` move the second cursor left/right. 184 | 185 | Note, even these two things could coexist, bidirectional is **not** compatible with JavaScript iterator protocol, because JavaScript iterators are one-shot consumption, and produce `{done: true}` if all values are consumed, and it is required that `next()` always returns `{done: true}` after that, but `previous()` actually require to restore to previous, undone state. 186 | 187 | ## Prior art 188 | - Python [iterable unpacking](https://www.python.org/dev/peps/pep-3132/) 189 | - Ruby [array decomposition](https://docs.ruby-lang.org/en/2.7.0/doc/syntax/assignment_rdoc.html#label-Array+Decomposition) 190 | - CoffeeScript [destructuring assignment with splats](https://coffeescript.org/#destructuring) 191 | - Rust [subslice pattern](https://rust-lang.github.io/rfcs/2359-subslice-pattern-syntax.html) 192 | - Rust [std::iter::DoubleEndedIterator](https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html) 193 | - Rust [Macro improved_slice_patterns::destructure_iter](https://docs.rs/improved_slice_patterns/2.0.1/improved_slice_patterns/macro.destructure_iter.html) 194 | 195 | ## Previous discussions 196 | - https://github.com/tc39/proposal-array-last/issues/31 197 | - https://github.com/tc39/proposal-reverseIterator/issues/1 198 | - https://es.discourse.group/t/bidirectional-iterators/339 199 | 200 | ## Old discussions 201 | - https://esdiscuss.org/topic/early-spread-operator 202 | - https://esdiscuss.org/topic/parameter-lists-as-arguments-destructuring-sugar#content-3 203 | - https://mail.mozilla.org/pipermail/es-discuss/2012-June/023353.html 204 | - http://web.archive.org/web/20141214094119/https://bugs.ecmascript.org/show_bug.cgi?id=2034 205 | - https://esdiscuss.org/topic/rest-parameter-anywhere 206 | - https://esdiscuss.org/topic/rest-parameters 207 | - https://esdiscuss.org/topic/strawman-complete-array-and-object-destructuring 208 | - https://esdiscuss.org/topic/an-update-on-rest-operator 209 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Proposal Title Goes Here

Stage -1 Draft / July 21, 2022

Proposal Title Goes Here

1855 | 1856 | 1857 |

1 This is an emu-clause

1858 |

This is an algorithm:

1859 |
  1. Let proposal be undefined.
  2. If IsAccepted(proposal),
    1. Let stage be 0.
  3. Else,
    1. Let stage be -1.
  4. Return ? ToString(proposal).
1860 |
1861 |

A Copyright & Software License

1862 | 1863 |

Copyright Notice

1864 |

© 2022 Your Name(s) Here

1865 | 1866 |

Software License

1867 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

1868 | 1869 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1870 | 1871 |
    1872 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 1873 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 1874 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 1875 |
1876 | 1877 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1878 | 1879 |
1880 |
--------------------------------------------------------------------------------