├── complete.js └── readme.md /complete.js: -------------------------------------------------------------------------------- 1 | // 2 | // This includes all code samples written as they would be once the pattern matching epic is fully 3 | // applied. 4 | 5 | // Complex cases are delegate to simple function that return true or false. 6 | function maybeRetry(res) { 7 | return res.status == 500 && !this.hasRetried; 8 | } 9 | 10 | // example 1 11 | match (res) { 12 | let when { status: 200, body, ...rest }: handleData(body, rest) 13 | let { destination: url } when { status and status >= 300 and status < 400, destination}: 14 | handleRedirect(url) 15 | when maybeRetry.bind(this): { // can alternatively be a higher order function 16 | retry(req); 17 | this.hasRetried = true; 18 | } 19 | 20 | default: throwSomething(); 21 | } 22 | 23 | // example 2 24 | match (command) { 25 | let [, dir] when [ 'go', ('north' or 'east' or 'south' or 'west')]: go(dir); 26 | let [, item] when [ 'take', (/[a-z]+ ball/ and { weight })]: take(item); 27 | default: lookAround(); 28 | 29 | 30 | // example 3 31 | match (res) { 32 | let when { data: [page] }: ... 33 | let when { data: [frontPage, ...pages ]}: ... 34 | default: { ... } 35 | } 36 | 37 | // example 4 38 | match (arithmeticStr) { 39 | let { groups: [left, right]} when (/(?\d+) \+ (?\d+)/): process(left, right); 40 | let [, left, right] when (/(\d+) \* (\d+)/: process(left, right); 41 | default: ... 42 | } 43 | 44 | // example 5 45 | const LF = 0x0a; 46 | const CR = 0x0d; 47 | 48 | match (nextChar()) { 49 | when LF: ... 50 | when CR: ... 51 | default: ... 52 | } 53 | 54 | // Option example 55 | class Option { 56 | #value; 57 | #hasValue = false; 58 | 59 | constructor (hasValue, value) { 60 | this.#hasValue = !!hasValue; 61 | if (hasValue) { 62 | this.#value = value; 63 | } 64 | } 65 | 66 | get value() { 67 | if (this.#hasValue) return this.#value; 68 | throw new Exception('Can’t get the value of an Option.None.'); 69 | } 70 | 71 | static Some(val) { 72 | return new Option(true, val); 73 | } 74 | 75 | static None() { 76 | return new Option(false); 77 | } 78 | 79 | static { 80 | Option.Some[Symbol.matcher] = (val) => ({ 81 | matched: #hasValue in val && val.#hasValue, 82 | value: val.value, 83 | }); 84 | 85 | Option.None[Symbol.matcher] = (val) => ({ 86 | matched: #hasValue in val && !val.#hasValue 87 | }); 88 | } 89 | } 90 | 91 | match (result) { 92 | // note, we are returning the unwrapped value, so we don't need destructuring 93 | let val when Option.Some: console.log(val); 94 | when Option.None: console.log("none"); 95 | } 96 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Introducing Epics. 2 | 3 | My proposal here is to split pattern matching into a well defined epic. This will be the first 4 | proposal following what may be a new process for us. That is, tracking groups of proposals 5 | intentionally. 6 | 7 | The goal of an epics process is 8 | * Ensure that we do not separate proposals from one another so that the context and meaning of 9 | their relationship is lost. 10 | * Ensure we are building from the language 11 | * Ensure we are working in small enough chunks that we have enough attention to attend to each 12 | one 13 | * Enable working on top of a clearly defined heirarchy of proposals 14 | 15 | An epic builds relationships between proposals. They build a topology, and allow other, smaller 16 | proposals to be considered as part of a whole. Identifying a Layer at which a proposal lives at 17 | can help with implementation planning, as well as determining the design space we are working 18 | with. 19 | 20 | When an epic is accepted in committee: it is accepted as a layered structure where each layer has 21 | explicit support from the committee for exploration. Some proposals on a given layer are critical 22 | path, others are not. They are noted as such. Each proposal has it's own problem statement and 23 | motivation. 24 | 25 | Identifying the layers of an epic takes the following process: 26 | * Determine what the core underlying functionality is. 27 | * Identify its immediate dependants. 28 | * Repeat until you have a tree of proposals. 29 | 30 | Proposals can move up and down in an epic, as their requirements change. 31 | 32 | Epics themselves have only 3 stages: Pending, In progress, and Completed. 33 | * A Pending Epic is under discussion in the committee. 34 | * An In Progress Epic has been accepted, and its constituent parts are being worked on. 35 | * A Completed Epic had all critical parts completed and is considered done. 36 | 37 | This will be pretty scary for the champions, especially the early stages. So, to quell some 38 | fears, when all layers are applied, you will be able to write code like this: 39 | * [Examples with fully applied pattern matching](complete.js) 40 | 41 | What we want to answer here, is, how do we get there? 42 | 43 | 44 | # Pattern matching: A layered perspective. 45 | 46 | We need to start at the beginning. What is the underlying problem we want to solve, and what 47 | builds on top of solving that problem? 48 | 49 | Matcher helpers in isolation, without "match" or patterns 50 | fundamentally, a pattern is a question that returns "yes" or "no". 51 | We can implement this, at first, as functions. Functions are one of JavaScript's super powers. We 52 | overlook them often because its so easy to take them for granted, but they really enable a lot of 53 | things and are well understood by developers. 54 | 55 | 56 | The layers for now, based on my review: 57 | 58 | **Layer 1**: 59 | * Functionality: Enable basic support for complex matching through syntax 60 | * Base proposal (foundation) 61 | 62 | **Layer 2**: 63 | * Functionality: Enable modifying the pass-through value 64 | * Custom Matchers (critical) 65 | * Fixing Switch / Introducing Match (arguably critical) 66 | * Functionality: Enable new contexts for complex matching 67 | * Catch guards, 68 | * etc. 69 | 70 | **Layer 3**: 71 | * Functionality and Ergonomics: Introduce Pattern Matching Syntax for common matches. 72 | * Pattern Matching Syntax (critical) 73 | 74 | **Layer 4**: 75 | * Ergonomics (readability): Remove unnecessary duplication for check & assign 76 | * Let-When statements (critical) 77 | 78 | Each of these layers (and their associated critical proposals) would be part of the epic, and the 79 | epic would only attain Completion if all layers had their critical parts fulfilled. 80 | 81 | 82 | ## Layer 1: Base Proposal, 83 | No match syntax, no syntax. Only support for patterns. 84 | This first part comes from [my analysis](https://docs.google.com/document/d/1dVaSGokKneIT3eDM41Uk67SyWtuLlTWcaJvOxsBX2i0/edit) of the current proposal's syntax 85 | 86 | It would be a mistake for us to introduce something so far from what developers are used to, as 87 | it will confuse developers about how existing syntax works. Instead, we can decompose the syntax 88 | into two parts: The assignment, and the match. This can be done completely independently of the 89 | match statement. 90 | 91 | The necessary pieces: 92 | * Patterns: a keyword when that takes a function that returns a true or false value 93 | 94 | * Assignable patterns: [let,const,var] _ when: if the `when` clause is true, then the assignment 95 | keyword will destructure the object originally passed to when. 96 | 97 | This gives us light weight matchers that are highly customizable. This addresses the problem 98 | "there are no ways to match patterns beyond regular expressions for strings". 99 | We also remove the need for parentheses. 100 | 101 | 102 | ```javascript 103 | function isOk(response) { 104 | return response.status == 200; 105 | } 106 | 107 | function isOkPair(key, response) { 108 | return response.status == 200; 109 | } 110 | 111 | let { body } when isOk(response); 112 | const { body } when isOk(response); 113 | var { body } when isOk(response); 114 | 115 | // the equivalent today would be: 116 | 117 | let { body } = isOk(response) ? response : {}; 118 | const { body } = isOk(response) ? response : {}; 119 | var { body } = isOk(response) ? response : {}; 120 | 121 | // you get the idea. I'll use let for now. 122 | 123 | // if we ever allow let statements in if statements, we can do this. 124 | if (let { body } when isOk(value)) { 125 | handle(body); 126 | } 127 | 128 | // There is no equivalent today. 129 | 130 | // note: 131 | 132 | let foo = when isOK(value); // foo will be a boolean. This is also fine, but weird to use when here. Maybe it should be disallowed. 133 | ``` 134 | 135 | This can be used in many other cases 136 | ```javascript 137 | const responses = [ 138 | {status: 200, body: "a body"}, 139 | /* ... etc */ 140 | ] 141 | 142 | // continue if isOk is not true 143 | for (let { body } when isOk of responses) { 144 | handle(body); 145 | } 146 | ``` 147 | 148 | The equivalent today 149 | ```javascript 150 | for (let response of responses) { 151 | if (isOk(response) { 152 | handle(response.body); 153 | } 154 | } 155 | ``` 156 | 157 | Again, if we ever allow assignment in this case 158 | ```javascript 159 | while (let { body } when isOk(responses.pop())) { 160 | handle(body); 161 | } 162 | ``` 163 | 164 | Equivalent today 165 | 166 | ```javascript 167 | while (responses.length()) { 168 | const response = responses.pop(); 169 | if (isOk(response) { 170 | handle(response.body); 171 | } 172 | } 173 | ``` 174 | 175 | If we are doing object iteration, then likely we have a reason to check the url 176 | and can handle that in a separate function. 177 | 178 | ```javascript 179 | const responseList = { 180 | "myURl": {status: 200, body: "a body"}, 181 | /* ... etc */ 182 | } 183 | 184 | function isOkPair([key, response]) { 185 | if (inAllowList(url)) { 186 | return response.status == 200; 187 | } 188 | return false; 189 | } 190 | 191 | for (let [url, { body }] when isOkPair in responseList) { 192 | handle(body); 193 | } 194 | ``` 195 | 196 | The equivalent today. 197 | ```javascript 198 | for (let [url, response] of responses) { 199 | if (isOkPair([url, response]) { 200 | handle(response.body); 201 | } 202 | } 203 | ``` 204 | 205 | ## Layer 2: Fixing `switch`. 206 | 207 | There are three problems in the initial problem statement that are being fixed here: 208 | 209 | 1) an explicit break is required in each case to avoid accidental fallthrough; 210 | 2) scoping is ambiguous (block-scoped variables inside one case are available in the scope of the others, unless curly braces are used); 211 | 3) the only comparison it can do is ===. 212 | 213 | Note: Match is actually optional. We don't actually need to introduce "match". 214 | We just need to enable switch to use patterns and assignable patterns: 215 | 216 | 217 | 218 | ```javascript 219 | function isGo(command) { 220 | const validDirections = ["north", "east", "south", "west"]; 221 | return command[0] === "go" && validDirections.includes(command[1]); 222 | } 223 | 224 | function isTake(command) { 225 | const isValidItemString = /[a-z+ ball]/; 226 | return command[0] === "take" 227 | && isValidItemString.match(command[1]) 228 | && command[1].weight; 229 | } 230 | 231 | switch (command) { 232 | let [, dir] when isGo: go(dir); 233 | let [, item] when isTake: take(item); 234 | default: lookAround(); 235 | } 236 | ``` 237 | 238 | But if we want to keep legacy behavior separate, then we can do this by introducing `match`. So 239 | lets say we have a new statement match. 240 | 241 | I am luke-warm on "killing switch". I think this isn't a worthwhile use of a keyword. Everything done 242 | from this point on with match could equally be done with switch, and this would free match to be 243 | used elsewhere. 244 | 245 | ```javascript 246 | function isGo(command) { 247 | const validDirections = ["north", "east", "south", "west"]; 248 | return command[0] === "go" && validDirections.includes(command[1]); 249 | } 250 | 251 | function isTake(command) { 252 | const isValidItemString = /[a-z+ ball]/; 253 | return command[0] === "take" 254 | && isValidItemString.match(command[1]) 255 | && command[1].weight; 256 | } 257 | 258 | match (command) { 259 | let [, dir] when isGo: go(dir); 260 | let [, item] when isTake: take(item); 261 | default: lookAround(); 262 | } 263 | 264 | function maybeRetry(res) { 265 | return res.status == 500 && !this.hasRetried; 266 | } 267 | 268 | match (res) { 269 | let { status, body, ...rest } when { status: 200}: handleData(body, rest) 270 | let { destination: url } when { status and status >= 300 and status < 400 }: 271 | handleRedirect(url) 272 | when maybeRetry.bind(this): { // can alternatively be a higher order function 273 | retry(req); 274 | this.hasRetried = true; 275 | } 276 | 277 | default: throwSomething(); 278 | } 279 | ``` 280 | 281 | With just these pieces, we can implement a more complex use case, which is Option matching! 282 | This would make a good proposal! with Option, Ok, None, Error etc. 283 | 284 | ```javascript 285 | class Option { 286 | #value; 287 | #hasValue = false; 288 | 289 | constructor (hasValue, value) { 290 | this.#hasValue = !!hasValue; 291 | if (hasValue) { 292 | this.#value = value; 293 | } 294 | } 295 | 296 | get value() { 297 | if (this.#hasValue) return this.#value; 298 | throw new Exception('Can’t get the value of an Option.None.'); 299 | } 300 | 301 | isSome() { 302 | return !!this.#hasValue; 303 | } 304 | 305 | isNone() { 306 | return !this.#hasValue; 307 | } 308 | 309 | static Some(val) { 310 | return new Option(true, val); 311 | } 312 | 313 | static None() { 314 | return new Option(false); 315 | } 316 | } 317 | 318 | // the is methods can of course be static, there is flexibility in how someone wants to implement this. 319 | match (result) { 320 | let { value } when result.isSome: console.log(value()); 321 | when result.isNone: console.log("none"); 322 | } 323 | ``` 324 | 325 | Similarily, builtins can all have an is brand check 326 | ```javascript 327 | match (value) { 328 | when Number.isNumber: ... // currently missing 329 | when BigInt.isBigInt: ... // currently missing 330 | when String.isString: ... // currently missing 331 | when Array.isArray: ... 332 | default: ... 333 | } 334 | ``` 335 | 336 | The bar to implement this stuff by users is low, as we are just working with functions. 337 | 338 | ## Layer 2: Custom Matchers 339 | 340 | There are cases where we want custom behavior -- where the object is not passed through 341 | unmodified to the let statement. This can't be implemented with a function that returns true 342 | or false. So what do we do here? In this case we want special behavior. 343 | 344 | A good motivating example is regex. This is the motivating case for custom matchers. 345 | Regex returns the matched value, and it would make sense for _this_ to be what we operate on, 346 | rather than the initial value. 347 | 348 | ```javascript 349 | Builtin Regex { 350 | static { 351 | Regex[Symbol.matcher] = (val) => ({ 352 | matched: // ..., 353 | value: // ..., 354 | }); 355 | } 356 | } 357 | 358 | match (arithmeticStr) { 359 | let { groups: [left, right]} when (/(?\d+) \+ (?\d+)/): process(left, right); 360 | let [, left, right] when (/(\d+) \* (\d+)/: process(left, right); 361 | default: ... 362 | } 363 | ``` 364 | 365 | Custom matchers can be implemented in user code 366 | ```javascrpt 367 | function equalityMatcher(goal, brand) { 368 | return function(test) { 369 | return goal.checkBrand(test) && goal === test; 370 | } 371 | } 372 | 373 | const LF = 0x0a; 374 | const CR = 0x0d; 375 | 376 | // These are now exotic strings. Use imagination for this one. 377 | Object.setPrototypeOf(LF, Char); 378 | Object.setPrototypeOf(CR, Char); 379 | // or like whatever. 380 | LF[Symbol.matcher] = equalityMatcher(LF); 381 | CR[Symbol.matcher] = equalityMatcher(CR); 382 | 383 | match (nextChar()) { 384 | when LF: ... 385 | when CR: ... 386 | default: ... 387 | } 388 | ``` 389 | 390 | This also means, we can now write option like so: 391 | 392 | ```javascript 393 | class Option { 394 | #value; 395 | #hasValue = false; 396 | 397 | constructor (hasValue, value) { 398 | this.#hasValue = !!hasValue; 399 | if (hasValue) { 400 | this.#value = value; 401 | } 402 | } 403 | 404 | get value() { 405 | if (this.#hasValue) return this.#value; 406 | throw new Exception('Can’t get the value of an Option.None.'); 407 | } 408 | 409 | static Some(val) { 410 | return new Option(true, val); 411 | } 412 | 413 | static None() { 414 | return new Option(false); 415 | } 416 | 417 | static { 418 | Option.Some[Symbol.matcher] = (val) => ({ 419 | matched: #hasValue in val && val.#hasValue, 420 | value: val.value, 421 | }); 422 | 423 | Option.None[Symbol.matcher] = (val) => ({ 424 | matched: #hasValue in val && !val.#hasValue 425 | }); 426 | } 427 | } 428 | 429 | match (result) { 430 | // note, we are returning the unwrapped value, so we don't need destructuring 431 | let val when Option.Some: console.log(val); 432 | when Option.None: console.log("none"); 433 | } 434 | ``` 435 | 436 | ## Layer 3: Pattern Matching Syntax 437 | Introducing pattern matching. A short hand for describing object shapes, that can be used with 438 | `when`. 439 | 440 | 441 | ### The (optional) Base Case: introducing `is` 442 | Let's rewind a bit and consider an early case. Given this pattern: 443 | 444 | ```javascript 445 | function isOk(response) { 446 | return response.status == 200; 447 | } 448 | 449 | let { body } when isOk(response); 450 | ``` 451 | 452 | What if we could rewrite it as: 453 | ```javascript 454 | let { body } when response is { status: 200 }; 455 | ``` 456 | 457 | We can also write it in if statements 458 | ```javascript 459 | if (when response is { status: 200 }) { 460 | // ... do work when response matches something 461 | } 462 | ``` 463 | 464 | In an unknown future, if potentially we allow the following: 465 | 466 | ```javascript 467 | if ( let x = someMethod()) { 468 | // ... do work when x is not null 469 | } 470 | ``` 471 | 472 | we could additionally allow: 473 | 474 | ```javascript 475 | if ( let { body } when response is { status: 200 }) { 476 | // ... do work when body is not null 477 | } 478 | ``` 479 | 480 | This is totally optional. This can, by the way, be dropped. Introducing an `is` keyword is totally optional. 481 | 482 | ### Implicit values 483 | Going back to a more orthodox case, we have implicit values. 484 | 485 | ```javascript 486 | match (command) { 487 | let [, dir] when [ 'go', ('north' or 'east' or 'south' or 'west')]: go(dir); 488 | let [, item] when [ 'take', (/[a-z]+ ball/ and { weight })]: take(item); 489 | default: lookAround(); 490 | } 491 | ``` 492 | 493 | However, implicit values can also apply to other proposals, as we are no longer tied to 494 | the match statement. Consider 495 | ```javascript 496 | try { 497 | something(); 498 | } catch when isStatus500 { 499 | // handle it one way... 500 | } catch when isStatus402 { 501 | // handle the other way... 502 | } catch (err) { 503 | // catch all... 504 | } 505 | ``` 506 | 507 | Something like this could be a dependency of layer 2 work, and eventually get the same benefits 508 | from layer 4 work. 509 | 510 | ```javascript 511 | try { 512 | something(); 513 | } catch when {status: 500} { 514 | // handle it one way... 515 | } catch when {status: 402} { 516 | // handle the other way... 517 | } catch (err) { 518 | // catch all... 519 | } 520 | ``` 521 | 522 | 523 | A more complex example is this one (without the if statement): 524 | ```javascript 525 | match (res) { 526 | let { data: [page] } when { data: [page] }: ... 527 | let { data: [frontPage, ...pages ]} when { data: [frontPage, ...pages ]}: ... 528 | default: { ... } 529 | } 530 | ``` 531 | 532 | This isn't ideal as we are repeating ourselves. 533 | So, we might fall back on functions here: 534 | 535 | ```javascript 536 | function hasOnePage(arg) { arg.data.length === 1 } 537 | function hasMoreThanOnePage(arg) { arg.data.length > 1 } 538 | match (res) { 539 | let { data: [page] } when hasOnePage: ... 540 | let { data: [frontPage, ...pages ]} when hasMoreThanOnePage: ... 541 | default: { ... } 542 | } 543 | ``` 544 | 545 | We can consider primatives, where we can default to ===: 546 | ```javascript 547 | const LF = 0x0a; 548 | const CR = 0x0d; 549 | 550 | // default to === for primatives 551 | match (nextChar()) { 552 | when LF: ... 553 | when CR: ... 554 | default: ... 555 | } 556 | 557 | match (nextNum()) { 558 | when 1: ... 559 | when 2: ... 560 | default: ... 561 | } 562 | 563 | // works the same way in single matchers. 564 | let nums = [1, 1, 1, 1, 1, 2, 1, 1, 1, 1] 565 | while (when 1 is nums.pop()) { 566 | count++ 567 | } 568 | 569 | // something additional to consider 570 | const responses = [..alistofresponses]; 571 | while (let {body} when {status: 200} is responses.pop()) { 572 | handle(body); 573 | } 574 | ``` 575 | 576 | ## Layer 4: Let-When statements 577 | One of the criticisms I have, for the current proposal, is the unforgiving conflation of 578 | assignment and testing. The separation of these two parts allows this proposal to be split into 579 | smaller chunks. However, there is a benefit to having conflation. recall this unfortunate example: 580 | 581 | 582 | ```javascript 583 | match (res) { 584 | let { data: [page] } when { data: [page] }: ... 585 | let { data: [frontPage, ...pages ]} when { data: [frontPage, ...pages ]}: ... 586 | default: { ... } 587 | } 588 | ``` 589 | 590 | This largely fell out of the previous proposals. However. this is a case where we want intentional 591 | conflation. For that, we can have `let when`. 592 | 593 | ```javascript 594 | match (res) { 595 | let when { data: [page] }: ... 596 | let when { data: [frontPage, ...pages ]}: ... 597 | default: { ... } 598 | } 599 | ``` 600 | 601 | Since this has been worked on in a layered way, this applies to the language more broadly: 602 | 603 | ```javascript 604 | while (let when {status: 200, body} is responses.pop()) { 605 | handle(body); 606 | } 607 | ``` 608 | 609 | A couple of edge cases: 610 | ```javascript 611 | let nums = [1, 1, 1, 1, 1, 2, 1, 1, 1, 1] 612 | while (when 1 is nums.pop()) { 613 | count++ 614 | } 615 | 616 | // this will throw, because you can't assign primitives. 617 | while (let when 1 is nums.pop()) { 618 | count++ 619 | } 620 | 621 | // this won't throw 622 | while (let x when 1 is nums.pop()) { 623 | count += x 624 | } 625 | ``` 626 | 627 | // Finally, we can have very intentional aliasing, without the shadowing issue: 628 | ```javascript 629 | match (res) { 630 | let when { status: 200, body, ...rest }: handleData(body, rest) 631 | let { destination: url } when { status and status >= 300 and status < 400, destination}: 632 | handleRedirect(url) 633 | when maybeRetry.bind(this): { // can alternatively be a higher order function 634 | retry(req); 635 | this.hasRetried = true; 636 | } 637 | 638 | default: throwSomething(); 639 | } 640 | ``` 641 | 642 | ## Conclusion 643 | 644 | We don't get 100% back to where we were with in the pattern matching proposal. We get 90% of the way there, but 645 | we also reuse existing structures and do it in a way that is learnable and consistent for programmers. Finally, 646 | there is room to expand here, this is by no means the final shape of a potential epic. 647 | 648 | I have to stop writing as my wrist is completely destroyed. I hope you will understand if I missed 649 | stuff or mistyped (for example i know that parentheses may be necessary). 650 | --------------------------------------------------------------------------------