8 ms·
Pattern Matching with TypeScript
- stupidcar 9y agoTC39 published its stage 0 proposal for pattern matching in ECMAScript a couple of days ago: https://github.com/tc39/proposal-pattern-matching https://github.com/tc39/proposal-pattern-matching
- deleted 9y ago[deleted]
- Avshalom 9y agosteal Haxe's, they even already have code to compile it to JS. http://haxe.org/manual/lf-pattern-matching.html http://haxe.org/manual/lf-pattern-matching.html
- alangpierce 9y agoCool to see a post about this sort of thing! I came up with a similar pattern a while ago (using Flow instead of TypeScript) for a lambda calculus evaluator. I ended up using a code generation approach so I define my datatypes in one file and the `match` method and `Matcher` interface are generated for me. Here's where I define the five cases for a lambda calculus expression undergoing evaluation: https://github.com/alangpierce/LambdaCalculusPlayground/blob/master/typeDefs.js#L137 https://github.com/alangpierce/LambdaCalculusPlayground/blob... Here are the autogenerated types for those cases (there's other code to make it work at runtime): https://github.com/alangpierce/LambdaCalculusPlayground/blob/master/types.js.flow#L458 https://github.com/alangpierce/LambdaCalculusPlayground/blob... And here's a simple example of using it: https://github.com/alangpierce/LambdaCalculusPlayground/blob/master/ExpressionEvaluator.js#L231 https://github.com/alangpierce/LambdaCalculusPlayground/blob...
- skybrian 9y agoThe last example is a classic visitor pattern. The earlier ones (using structs of functions) are a variation on this that seems a bit closer to the spirit of pattern matching.
- swissmanu 9y agohi, i am the author of the article. > The last example is a classic visitor pattern you are absolutely right about the visitor pattern. will add the specific reference to the pattern in a revised version. thanks for pointing out!
- phpnode 9y agoIt would be nice to be able to use the annotations directly, we support this in flow-runtime[0] but I think it would be fairly straightforward to support in TypeScript too, It looks like this: import t from 'flow-runtime'; const makeUpperCase = t.pattern( (input: string) => input.toUpperCase(), (input: number) => input ); console.log(makeUpperCase("foo bar")); console.log(makeUpperCase(123)); console.log(makeUpperCase(false)); // throws [0] https://codemix.github.io/flow-runtime/#/docs/pattern-matching https://codemix.github.io/flow-runtime/#/docs/pattern-matchi...
- koolba 9y agoShouldn't the uppercase of a number be a string? const makeUpperCase = t.pattern( (input: string) => input.toUpperCase(), (input: number) => '' + input ); Otherwise you get odd behaviours like: makeUpperCase('1') + makeUpperCase('1') = '11' (probably what you want) makeUpperCase(1) + makeUpperCase(1) = 2 (probably not what you want)
- phpnode 9y agoYeah probably, but it's just an example
- rezistik 9y ago...wait. Why would the uppercase of '1' be '11'? It would just be '1'.
- livingparadox 9y agoI had to reread it a few times to parse it. Its result of "adding" the uppercase of 1 that should be 11, not the uppercase itself. In other words, the result of "makeUppercase" should be a string, not a number.
- rezistik 9y agoI see it now.
- deleted 9y ago[deleted]
- jez 9y agoPattern matching without support for algebraic datatypes really misses the point. What you want is to be able to declare all the shapes of your data, with descriptive names, and then be able to say definitely "these are the only such shapes." Pattern matching without exhaustiveness checking and algebraic datatypes is really just a disguise: syntax level changes when the issue is a semantic (i.e. language-level) problem.
- alangpierce 9y agoThe `Payment` example in the post is effectively an implementation of algebraic datatypes. `PaymentPattern` is how the code specifies "these are the only such shapes", and exhaustiveness is checked because the typechecker won't let you provide a `PaymentPattern` that omits any cases. It's certainly not as flexible as pattern matching like you'd get in a serious functional language, but I think it handles the use case "condition over cases of an algebraic type" just fine.
- z1mm32m4n 9y ago> The `Payment` example in the post is effectively an implementation of algebraic datatypes. Algebraic data types are what we call "initial": they're characterized by how they're built up structurally. The 'PaymentMatcher' example, while using the terms "matcher" and "pattern" in the variable names, is actually closer to a "final" solution, since they're characterized by how they behave. This is a bit abstract, but to put it simply, the key difference between what you and I are talking about is the same difference between algebraic datatypes and type classes. Algebraic datatypes are built up with constructors, and then exist structurally. On the other hand, type classes are more ephemeral. When declaring a type class, we say "these N methods are the ways you can 'poke' and 'prod' me to get a response," that is, it's the behavior of the type class that's most important. So in fact, it's not that algebraic datatypes are more flexible than type classes or vice versa, but rather that they're duals of each other; they codify similar concepts, but approach the problem from two different sides. I find this sort of stuff fascinating! Some of my favorite articles dealing with these sorts of nuances: - Typed Tagless Final Interpretters[1] - Practical Foundations for Programming Languages, Chapter 15 "Inductive and Coinductive Types"[2] [1]: http://okmij.org/ftp/tagless-final/course/lecture.pdf http://okmij.org/ftp/tagless-final/course/lecture.pdf [2]: http://www.cs.cmu.edu/~rwh/pfpl.html http://www.cs.cmu.edu/~rwh/pfpl.html
- kmicklas 9y agoIt's amazing that people still design languages without algebraic data types.
- peterkelly 9y agoYou can declare union types in TypeScript: type BinaryNode<T> = { tag: 'leaf', value: T } | { tag: 'branch', left: BinaryNode<T>, right: BinaryNode<T> }; Then things like the following will typecheck correctly: function traverse<T>(node: BinaryNode<T>): void { switch (node.tag) { case 'leaf': console.log(node.value); break; case 'branch': traverse(node.left); traverse(node.right); break; } } In this example, once the compiler recognizes the tag is 'leaf', it knows there is a node property and will report an error if you try to access the left or right fields, which do not exist. The reverse is true for 'branch'. Not as nice as Haskell for sure, but the presence of union types is something I've found very handy. You can also declare tuple types: type MinMaxAvg = [number, number, number]; and do pattern deconstruction (though not pattern matching) in function arguments and assignments: function test([min, max, avg]: MinMaxAvg): void { // ... }
- deleted 9y ago[deleted]
- jahewson 9y agoWell, that was disappointing. I thought we left this kind of over-abstracted code behind with Java. A simple one-level switch on typeof or instanceof really does not deserve to be called "pattern matching". Given that TypeScript supports tagged union types it would make far more sense to actually use them and stop using instanceof all together. If are we going to do this kind of thing why not a least do it simply: switch(typeof x) { case "string": or switch(x.constructor) case Thing: Ok, so TypeScript can't handle the types for this yet but it's valid JS so you get the idea.
- masklinn 9y ago> If are we going to do this kind of thing why not a least do it simply: I expect it's because actual type-switching allows the compiler to type-assert at the same time. So it knows what within the String branch the value is an actual string. Whereas typeof() is severely limited (only primitive types) and switching on an arbitrary property tells pretty much nothing to the compiler.
- jahewson 9y agoYes, that's the current state of things. But support for control-flow typing of my above two examples seems like an easy addition to the TypeScript compiler - if that's what you want. Personally I'd like to see JS support for proper pattern matching, i.e. TC39, then TypeScript can build on that.
- masklinn 9y ago> But support for control-flow typing of my above two examples seems like an easy addition to the TypeScript compiler - if that's what you want. The first one is not useful and the second one is nonsensical (as there is no guarantee that there's any relation between #constructor and the type of the variable)
- agentgt 9y agoThe technique mentioned in the article of using an interface for pseudo pattern matching with anonymous implementation is basically the visitor pattern (yes I know patterns are BS once your language is sufficiently powerful but lets ignore that). It is basically how you implement pattern matching for any language that has OOP dispatch but lacks structural typing (but hopefully has anonymous classes). One way to facilitate the above pattern is to use a code generator. A Java one I have played with is derive4j [1]. I'm not sure if typescript has a preprocessor but that would be one way to make the lack of structural pattern matching easier to deal with. [1]: https://github.com/derive4j/derive4j https://github.com/derive4j/derive4j
- x1000 9y agoThis article seems relevant, https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions https://www.typescriptlang.org/docs/handbook/advanced-types.... . Particularly the part on discriminated unions. I'm just now learning TypeScript, so I have yet to actually implement anything this way.
- peterkelly 9y agoCombining switch statements with destructuring [1] can be very useful: type ASTNode = { tag: 'BinOp', op: string, left: ASTNode, right: ASTNode } | { tag: 'UnaryOp', op: string, arg: ASTNode } | { tag: 'Literal', value: number }; function evaluate(node: ASTNode): number { switch (node.tag) { case 'BinOp': { const { op, left, right } = node; return evalBinary(op, evaluate(left), evaluate(right)); } case 'UnaryOp': { const { op, arg } = node; return evalUnary(op, evaluate(arg)); } case 'Literal': { const { value } = node; return value; } } } [1] https://ponyfoo.com/articles/es6-destructuring-in-depth https://ponyfoo.com/articles/es6-destructuring-in-depth
- masklinn 9y agoThat's just sad if you've used languages with actual pattern matching though. It's like C's unsafe unions being back with a furor.
- smitherfield 9y agoThe original code with the switch statement is by far the easiest to read, as well as being by far the fastest and most concise to write and (though it’s unlikely to matter) by several orders of magnitude the fastest to run as well. The proper way to pattern match on types in JavaScript (and therefore TypeScript) is to first of all not if you can avoid it, but failing that a simple if-else chain checking `typeof(arg) === "primitiveType"` and `arg instanceof ReferenceType`, with ample use of the `debugger` statement to ensure inputs have the types and map to the branches you’re expecting them to. TypeScript is great for adding type safety and better self-documenting qualities to a JS codebase. But attempting to preserve the semantics of its “imaginary” extensions to the JS type system (interfaces, generics) at runtime is a recipe for bugs, severe confusion and reams of pointless boilerplate.
- barkathemoon 9y agoThere's pattern matching for javascript already: https://github.com/z-pattern-matching/z https://github.com/z-pattern-matching/z , works pretty good, also match objects, very powerful
- twfarland 9y agoThis looks a bit overdone. For the first example I'd just do it like this: const numbers = [,'one','two','three'] function matchNumber(n: number) { return numbers[n] || n }
- z3t4 9y agofunction calculatePaymentAmount(payment) { if(typeof payment.payByCard != "number") throw new Error("payment.payByCard=" + payment.payByCard); if(typeof payment.cardFee != "number") throw new Error("payment.cardFee=" + payment.cardFee); if(typeof payment.payByCash != "number") throw new Error("payment.payByCash=" + payment.payByCash); if(typeof payment.cashDiscount != "number") throw new Error("payment.cashDiscount=" + payment.cashDiscount); if(payment.payByCash <= 0 && payment.cashDiscount != 0) throw new Error("payment.payByCash=" + payment.payByCash + " payment.cashDiscount=" + payment.cashDiscount); return payment.payByCard + payment.payByCard * payment.cardFee + payment.payByCash - payment.cashDiscount; } When an error is thrown, a core-dump should be generated, logged, and a developer notified, and the app should restart. The compiler can only catch so many bugs.
- gibbitz 9y agoI saw a talk using interfaces to validate API calls/responses with typescript. I've been working in dynamically typed languages for a long time and I've built up habits that make it so I rarely run into issues where I am expecting a Number and get a String or that I can't determine this as the cause quickly, so typescript didn't appeal to me previously. Uisng interfaces to enforce uniform object structures is a game changer though and a use for typrscript that I didn't anticipate.
- styfle 9y agoI actually discovered this in the TypeScript 1.x days (2013 I think) but I didn't even consider it pattern matching. I was probably calling it a type safe switch statement. As the article points out, the value in doing this is if you have a lot of calling code that creates a switch statement, say on a "type". This solution will give compiler errors at all the callers if you add a new type, but a switch statement will not and just hit the default.