└── README.md /README.md: -------------------------------------------------------------------------------- 1 | Generator Expressions for ECMAScript 2 | ------------------------------------ 3 | 4 | A convenient way of generating arrays/sets/maps of values in a contained expression. It's an alternative to [array comprehensions](http://tc39wiki.calculist.org/es6/array-comprehensions/) and designed to pair well with [`do` expressions](https://github.com/tc39/proposal-do-expressions). I think of it as the multi-value form of do-expressions. 5 | 6 | ## Motivation 7 | 8 | A strong argument allowing traditional for comprehensions in the language is that when they become complex, they're often better structured as combinator methods with arrow functions. Therefore it's better to just start with combinators from the start to make that transition easier. 9 | 10 | However, we've observed that when combinator chains become complex, rather than staying in the combinator form, people tend to break out of the combinator form and switch to imperative loops. 11 | 12 | The idea around this proposal is to allow a more natural transition by making a convenient shorthand imperative style that can be expanded to more complex cases. 13 | 14 | ## Generator Expressions 15 | 16 | The primitive is a new kind of expression that allows `yield`ing. It evaluates to a generator. 17 | 18 | ```js 19 | let gen = *{ 20 | yield 1; 21 | yield 2; 22 | }; 23 | ``` 24 | 25 | This desugars to an immediately invoked generator function: 26 | 27 | ```js 28 | let gen = (function*() { 29 | yield 1; 30 | yield 2; 31 | })(); 32 | ``` 33 | 34 | This can be combined with various initializers to create an Arrays, Maps or Sets. 35 | 36 | ```js 37 | let arrayOfUsers = [...*{ 38 | for (let user of users) 39 | if (user.name.startsWith('A')) 40 | yield user; 41 | }]; 42 | ``` 43 | 44 | ```js 45 | let arrayOfUsers = Array.from(*{ 46 | for (let user of users) 47 | if (user.name.startsWith('A')) 48 | yield user; 49 | }); 50 | ``` 51 | 52 | ```js 53 | let setOfUsers = new Set(*{ 54 | for (let user of users) 55 | if (user.name.startsWith('A')) 56 | yield user; 57 | }); 58 | ``` 59 | 60 | ```js 61 | let mapOfUsers = new Map(*{ 62 | let i = 0; 63 | for (let user of users) 64 | if (user.name.startsWith('A') && (i++ % 2) === 0) 65 | yield [user.id, user]; 66 | }); 67 | ``` 68 | 69 | ## Code Evolution 70 | 71 | Since these generator expressions naturally expand to more complex examples, you can keep expanding these with more complex logic as requirements expand. While still remaining in an isolated expression. 72 | 73 | ```js 74 | let allUsers = new Set(*{ 75 | let i = 0; 76 | for (let user of activeUsers) { 77 | i++; 78 | let isEven = (i % 2 === 0); 79 | let id = user.id; 80 | let name = user.name; 81 | if (id && name !== 'DELETED') { 82 | yield { id, name, isEven }; 83 | } 84 | } 85 | for (let user of inactiveUsers) { 86 | i++; 87 | let isEven = (i % 2 === 0); 88 | yield { id: null, name: user.name, isEven }; 89 | } 90 | }); 91 | ``` 92 | 93 | If the code complexity keeps expanding to requiring a temporary array, the next refactor step is to switch to a `do` expression. 94 | 95 | ```js 96 | let allUsers = new Set(do { 97 | let tmp = []; 98 | let i = 0; 99 | for (let user of activeUsers) { 100 | i++; 101 | let isEven = (i % 2 === 0); 102 | let id = user.id; 103 | let name = user.name; 104 | if (id && name !== 'DELETED') { 105 | tmp.push({ id, name, isEven }); 106 | } 107 | } 108 | for (let user of inactiveUsers) { 109 | i++; 110 | let isEven = (i % 2 === 0); 111 | tmp.push({ id: null, name: user.name, isEven }); 112 | } 113 | tmp.reverse(); 114 | }); 115 | ``` 116 | 117 | ### Completion Values 118 | 119 | Just like do-expressions, the completion value is like a return value. That means that the completion value is the final value in the generator. This is an advanced and uncommon feature. In most common uses of this feature, the completion value is ignored. E.g. when used to construct Arrays or Sets. 120 | 121 | This means that this yields a generator whose final value is `z`. 122 | 123 | ```js 124 | let xyz = *{ 125 | for (let x in a) 126 | yield x; 127 | for (let y in b) 128 | yield y; 129 | z; 130 | }; 131 | ``` 132 | 133 | It desugars to: 134 | 135 | ```js 136 | let xyz = (function* { 137 | for (let x in a) 138 | yield x; 139 | for (let y in b) 140 | yield y; 141 | return z; 142 | })(); 143 | ``` 144 | 145 | This can be useful for scheduling code like Task.js. 146 | 147 | ```js 148 | let promise = Task(*{ 149 | const [foo, bar] = yield Task.join( 150 | read("foo.json"), 151 | read("bar.json") 152 | ); 153 | foo.x + bar.y; 154 | }); 155 | ``` 156 | 157 | ## `break` Statements 158 | 159 | `break` without a label will break out of the generator. It behaves like returning the completion value of a generator function. Early returns can be accomplished using `break`. 160 | 161 | ```js 162 | let promise = Task(*{ 163 | const [foo, bar] = yield Task.join( 164 | read("foo.json"), 165 | read("bar.json") 166 | ); 167 | if (!bar) { 168 | foo.x; 169 | break; 170 | } 171 | foo.x + bar.y; 172 | }); 173 | ``` 174 | 175 | You can break to labels inside of the generator expression just like normal. However, with a label defined outside the generator expression, the control flow doesn't make as much sense since in a free standing generator, that scope doesn't exist anymore. That's a syntax error. 176 | 177 | ```js 178 | foo: { 179 | let items = [...*{ 180 | break foo; // SyntaxError! 181 | }]; 182 | } 183 | ``` 184 | 185 | ## `return` Statements 186 | 187 | It's unclear what the `return` statement should do inside these generators. It could either return out of the outer function or abruptly stop the iteration of the generator. I'm leaning to just forbidding `return` for now. Early returns are awkward but works, which seems fine because the use case for the completion value is rare anyway. 188 | 189 | `throw` works just fine though. 190 | 191 | ## `do * { ... }` Syntax Alternative 192 | 193 | An alternative syntax could use the `do` prefix since it is effectively in the same category as `do` expressions and this might help explain it once you are familiar with `do`-expressions. I tend to prefer the shortest possible syntax when possible though. 194 | 195 | ## Related Proposals 196 | 197 | [`do` expressions](https://github.com/tc39/proposal-do-expressions) 198 | 199 | [Array comprehensions](http://tc39wiki.calculist.org/es6/array-comprehensions/) 200 | 201 | ## Prior Art 202 | 203 | [F# Sequence Expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/sequences) 204 | 205 | [Scala Sequence Comprehensions](https://docs.scala-lang.org/tour/sequence-comprehensions.html) 206 | 207 | ## [Status of this Proposal](https://github.com/tc39/ecma262) 208 | 209 | This has not yet been presented to TC39. I'm still gathering feedback. 210 | --------------------------------------------------------------------------------