-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Pattern Matching Support #165
Comments
We've typically avoided features that require type information at runtime, because many types in TypeScript are indistinguishable from a runtime perspective. For example, what would the emitted JavaScript look like in this case? enum Suit { Spades, Hearts, Clubs, Diamonds }
enum Rank { Ace, King, Queen, Jack }
function pickCard {
(s: Suit) : number { return 1; }
(r: Rank): number { return 0; }
}
pickCard(Suit.Spades);
pickCard(undefined); |
@RyanCavanaugh You could potentially build in an Ometa-like library that allowed you to do pattern matching of all sorts. You may want a more TS friendly syntax but it would be very compelling. You could do matching on any object not just Types. Here's an example |
"We've typically avoided features that require type information at runtime" Will that change with the accommodating of the angular2 reflection? |
@AlexGalays, i do not think so. one thing we are trying to maintain is keep a single module transpilation possible with no semantic information present. |
I have to say, something like this would be insanely nice. What are the chances of this happening? |
👍 |
3 similar comments
👍 |
👍 |
👍 |
Just wanted to point out a possible alternative that may be available with This gets you code that looks like this, and requires no changes to TypeScript whatsoeverinterface SomeType {
stringValue: string
}
function isSomeType(x: any): x is SomeType {
return x && typeof(x.stringValue) === 'string';
}
function foo(x: number | string | boolean | SomeType): string {
if(typeof x === 'number' || typeof x === 'boolean') {
return x.toString();
} else if (typeof x === 'string') {
return x;
} else if (isSomeType(x)) {
return x.stringValue;
} else {
return 'other';
}
}
function match<T, U>(value: T, matcher: (t: T) => U) {
return matcher(value);
} And we can use it like this: const res = match(1, (x: string | boolean | number | SomeType): string => {
if(typeof x === 'number' || typeof x === 'boolean') {
return x.toString();
} else if (typeof x === 'string') {
return x;
} else if (isSomeType(x)) {
return x.stringValue;
} else {
return 'other';
}
}); and const value: string | boolean | number | SomeType = 5;
const res = match(value, x => {
if(typeof x === 'number' || typeof x === 'boolean') {
return x.toString();
} else if (typeof x === 'string') {
return x;
} else if (isSomeType(x)) {
return x.stringValue;
} else {
return 'other';
}
}); With full safety that we have handled all cases in the union. New proposal for syntactic sugarMake A language construct that takes an object mapping the type (property name) to a function or literal. const res = match (value) {
string: s => s,
boolean: b => b.toString,
number: n => n.toString(),
SomeType({ stringValue }) { return stringValue },
default: 'other'
} All that's happening here is it creates a function for you that creates an So this maps out to the following code: const res = ((x: string | boolean | number | SomeType): string => {
if (typeof x === 'string') {
return ((s: string) => s)(x);
} else if (typeof x === 'boolean') {
return ((b: boolean) => b.toString())(x);
} else if(typeof x === 'number') {
return ((n: number) => n.toString())(x);
} else if (isSomeType(x)) {
return (({ stringValue }: SomeType) => stringValue)(x);
} else {
return 'other';
}
})(value); This somewhat addresses @RyanCavanaugh's concern about enum types or other things which have their type information erased at runtime in the sense that because no As an optimization, if the type being matched on is purely composed of a "tagged"/disjoint union based on type literals, it could output the following: type Circle = {
shape: 'square'
radius: number
}
type Rectangle = {
shape: 'rectangle'
length: number
width: number
}
type Square = {
shape: 'square'
length: number
}
// because these have a shared literal property
// that will be used as the implicit "tag", and TS
// can know this is a tagged union
type Shape = Circle | Rectangle | Square
const shape: Shape = getShape(); // some shape
function getArea(shape: Shape) {
return match shape {
Circle: ({ radius }) => 3.14 * (radius * radius),
Rectangle: ({ length, width }) => length * width,
Square: ({ length }) => length * length,
};
} The getArea function would be translated in an optimized way to look like this: function getArea(shape: Shape) {
return ({
/* Circle.shape literal */ 'circle': ({ radius }) => 3.14 * (radius * radius),
/* Rectangle.shape literal */ 'rectangle': ({ length, width }) => length * width,
/* Square.shape literal */ 'square': ({ length }) => length * length,
})[shape.shape](shape);
} |
👍 But why not erlang style function guards? The syntax proposed above is a little funky.
becomes:
or checking values:
|
Personally, I wouldn't call it funky, since it's pretty common (i.e. found a lot of adoption). Here's a incomplete list of languages that come to mind: There are ought to be plenty more, those just came to the top of my head. You can check values using guards then, i.e. Your suggestions reminds me of multi-methods in lisp or function lookup based on arity like in elixir. |
@Zvxy it sounds more like your asking for a feature to automatically combine function overloads into a single function. It's a nice idea, but it wouldn't work for interfaces unless TypeScript could automatically create an interface type guard, which is impossible in ambiguous cases. |
I couldn't really find a better word for it than "funky", but you're right, this isn't pattern matching and is actually arity overloading. I do code in rust occasionally (and elixir/haskell is love). Here's a quick way I had to do it in rust:
Rust is notoriously verbose. I'd like to just do this:
But I can't... You're right, I had forgotten about interfaces (I have been switching between languages). Do you have any alternative solutions? |
@Zvxy As @tejacques correctly pointed out we are talking about different things. But still, here is what this could look like in rust, using ADTs and matching: enum Foo {
Foo(String),
Bar(i32),
}
fn test(x: Foo, n: i32) -> i32 {
n * match x {
Foo::Foo(s) => s.parse::<i32>().unwrap(),
Foo::Bar(n) => n,
}
}
#[test]
fn it_works() {
assert_eq!(test(Foo::Foo("5".to_owned()), 5), 25);
assert_eq!(test(Foo::Bar(5), 5), 25);
} Not that verbose. I see where you are coming from, though. I missed function overloading by type for quite a while when leaving C#, but once I got the hang of ADTs, I don't really see the point anymore. |
@felixSchl yeah, I had edited my post right before you responded about it being arity overloading and not pattern matching, my mistake. You said you don't really see the point anymore, but as far as I'm aware that code is going to be checked during runtime, not compile time. This is going a bit off topic though. |
why not extending |
@alrz The answer is in your question - because it's a statement. See how in the rust example above I could multiply the result of the match expression? Statements are inflexible and enforce imperative style coding. Take try/catch, for example, or the for loop, while loop, any loop. Personally, I wish languages would part entirely from these constructs. Let's even take let y = if x == 0 then "foo" else "bar" which you would need to do imperatively using let y: string; // <- I have to tell the compiler the type and cannot at compile time reason about if this value is going to null or not :(
if (x == 0) {
y = "foo";
} else {
y = "bar";
} To be fair, there are ternary expressions in js/ts: const y = (x == 0) ? "foo" : "bar"; |
@Zvxy I think the only thing you can do for interfaces (at least that I can think of) is check the scope for a function that guards on that interface, but this is far from perfect. |
👍 |
You could do redundant pattern matching right now
But some syntax sugar would be appreciated. |
Any reason https://github.com/bendetat/SimplicityJS wouldn't work with TS? |
Pattern matching is one of the most useful features of "more powerful" languages like Swift, Scala and Rust. I'd highly welcome this feature as part of Typescript! |
@tejacques Do you have an update of your interesting example for typescript 2.0 ? |
This is definitely a feature that would make me switch to TypeScript for every project. Whenever you're dealing with a CQRS architecture, pattern matching has to come along the way. That'd allow people to create a strongly typed Flux / Redux implementation. For now, only Elm offers such possibility. Strongly-typed actions + type aliases + ADT + pattern matching are a bless when dealing with such architecture. You're basically covered that if the code compiles, it's gonna work pretty much without runtime exceptions. If you declare a In current implementations : function updateState(action, state) {
switch(action.type) { // a String...
case 'create':
var newTodo = action.data; // is data really an object ? shouldn't the compiler know ?
state[newTodo.id] = newTodo; // and if state is not purely a map ? what if todo as no 'id' ?
}
} With type algebra + pattern matching this would become (sorry for pseudo-code) :
Typescript seems to already offer the strongly-typed system, the destructuring ability. The only thing this example misses would be pattern matching to be a little more readable, and more safety, and let the compiler check for most of the errors that today occur in production (example : |
@aesteve In today's TypeScript 2.0 code, your 'pseudocode' can be written as follows and works fine: Preamble: interface Todo {
id: string
items: string[]
}
interface CreateTodo {
kind: 'create'
todo: Todo
}
interface RemoveTodo {
kind: 'remove'
id: string
}
type TodoAction = CreateTodo | RemoveTodo;
type SomeState = Map<string, Todo>;
function updateState(action: TodoAction, state: SomeState) {
switch (action.kind) {
case 'create': // here the compiler knows that action is CreateTodo, of {id: String, items: string[]}
case 'remove': state.delete(action.id)
default: const _exhaustiveCheck: never = action
}
} The argument about automatic exhaustivity checking for the above seems to have been had in #9260 (see the last few comments including my own for a summary). I've started to propose (but haven't had time yet to formalise) doing this with overloading in #10765, so that it would look like this (for the times that it's more convenient to write these separately): function updateState(action: CreateTodo, state: SomeState) {
// here the compiler knows that the action is CreateTodo
}
function updateState(action: DeleteTodo, state: SomeState) {
state.delete(action.id)
}
function updateState(action: TodoAction, state: SomeState): never {} // unclear if there's a better syntax for doing this bit |
Much of what is being asked for in pattern matching is already possible in TS 2.0.0 (Thanks!). It is achieved without storing any runtime type information too. But for me, the one thing that would be nice would be more intuitive syntax. For example, right now I can write code like this.
That's great. But something like this would just be easier to read:
My point here is that using this strictly with classes could avoid some of the "having to include runtime information" because you could piggy back off of what is already included (i.e., the same information that
My guess is that what's already been implemented with type narrowing could be completely reused here. As far as I can tell, this is just syntactic sugar for:
...with the same possibilities for detection mutual exhaustion. Or am I missing something? |
Here's another implementation of pattern matching for sum types using dynamic dispatch: // Just for readability
type Sum<Components> = Components
// Keys are the names/indices of the components, values are the types of the components
type SomeSumType = Sum<{
NUMBER: number
STRING: string
}>
// ST is for SumType
interface CaseOf<ST> {
match: <T>(pattern: Pattern<ST, T>) => T
}
// T is for TargetType
type Pattern<ST, T> = {
[key in keyof ST]: (cas: ST[key]) => T
}
const createCase = <ST, K extends keyof ST>(k: K, value: ST[K]): CaseOf<ST> => ({
match: <T>(pattern: Pattern<ST, T>) => pattern[k](value)
})
const a: CaseOf<SomeSumType> = createCase('NUMBER', 5)
const b: CaseOf<SomeSumType> = createCase('STRING', 'someString')
const notA: CaseOf<SomeSumType> = createCase('NUMBER', 'wrongType') // Type error
const wrongCase: CaseOf<SomeSumType> = createCase('C', 'test') // Type error
const match = <CS, T>(cas: CaseOf<CS>, pattern: Pattern<CS, T>) => cas.match(pattern)
const result = match<SomeSumType, boolean>(a, {
NUMBER: num => num > 6,
STRING: str => str === 'expectedString'
}) |
Achieving a similar effect with Church encoding, as per Rúnar Bjarnason: interface Shape {
match<T>(a: (x: Square) => T, b: (y: Rectangle) => T, c: (z: Circle) => T): T;
}
class Square implements Shape {
constructor(public size: number) { }
match<T>(a, b, c): T { return a(this); }
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) { };
match<T>(a, b, c): T { return b(this); }
}
class Circle implements Shape {
constructor(public radius: number) { };
match<T>(a, b, c): T { return c(this); }
}
let x: Shape = new Square(10);
console.log(x.match(
(sqr) => { return sqr.size * sqr.size },
(rec) => { return rec.width * rec.height },
(cir) => { return Math.PI * cir.radius ** 2 })); The major problem with this approach (in TypeScript) is that I see no way to prevent library consumers from subclassing |
I've used a similar pattern. I'm not sure the problem is with subclassing (implementing, really) To me, the bigger issue is when you have lots of mutually exclusive possibilities. It would end up being a bit awkward. But it works and, as I said, I have used this approach. |
Ah...I think I see what you mean by extending If you have an open ended set of possibilities, you are probably better of simply defining a If you really wanted to, I suppose you could do something like this:
I kind of like using named handlers (although it seems as though the type checker would always ensure you didn't have them mixed up). That cast is necessary to keep TypeScript from complaining. But it is still type safe since it is just loosening the semantics around object literals. It still ensures that the value actually has the structure of a |
That's still quite a lot of boilerplate compared to modern FP languages, plus we don't all use classes :p (I avoid them like the plague) |
How would you do proper pattern matching in TypeScript if types are erased at runtime? Isn't this pointless |
@goodmind JVM languages such as scala also have to deal with this issue because of type erasure. It's still a very useful tool to have nonetheless and read better than imperative if/else statements. See https://github.com/tc39/proposal-pattern-matching where everything can be checked at runtime. Although, Things like |
@AlexGalays It's stage 0 tho |
@AlexGalays As far as boilerplate goes, I totally agree. I love pattern matching in Rust, Scala, etc. So thanks for pointing out that proposal. That would be very cool! I'll have to keep an eye on that. |
A discriminated union-based approach may also be an option for you. As per the docs:
...which addresses the issue I mentioned earlier re: preventing programmers from adding new implementations of Note that exhaustiveness in the switch/case used in the interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function shape<T>(s: Shape, a: (x: Square) => T, b: (y: Rectangle) => T, c: (z: Circle) => T): T {
switch (s.kind) {
case "square": return a(s);
case "rectangle": return b(s);
case "circle": return c(s);
}
}
let x: Square = { "kind": "square", "size": 10 };
console.log(shape(x,
(sqr) => { return sqr.size * sqr.size },
(rec) => { return rec.width * rec.height },
(cir) => { return Math.PI * cir.radius ** 2 })); |
@laser You can already do very general discriminator-based pattern matching in JS by doing something like: const match = (p, ...c) => {
const cases = new Map(c)
return x => cases.get(p(x))(x)
}
const area = match(
x => x.kind,
["square", ({ size }) => size ** 2],
["rectangle", ({ width, height }) => width * height],
["circle", ({ radius }) => Math.PI * radius ** 2])
console.log(area({ "kind": "square", "size": 10 }))
// => 100 Unfortunately, the typechecker suffers from a kind of death by thousand cuts of missing features here, and while it's almost possible to implement a type definition for |
Just saw this issue and thought I'd put in a plug: https://github.com/pelotom/unionize You can have pattern matching today! |
@pelotom That looks very cool. I really like the design. I try to use the tagged union approach whenever I can and you seem to have really cleared out the boilerplate for most use cases. I'll definitely check it out! |
One aspect that's important for pattern matching is to not force the requirement of providing an explicit discriminator onto the input data; it should be possible to pattern match on data in its "natural", messy state. This is important to be able to get all the benefits of pattern matching from other languages (e.g. pattern matching over E.g. for the shapes thing above, it should be possible to do: const hasProp = (o, p) => o.hasOwnProperty(p)
const area = match(
x => hasProp(x, "size") ? "square" : hasProp(x, "radius") ? "circle" : "rectangle",
["square", ({ size }) => Math.pow(size, 2)],
["rectangle", ({ width, height }) => width * height],
["circle", ({ radius }) => Math.PI * Math.pow(radius, 2)]) And then be able to match over "raw" data that is not explicitly tagged or prepped for pattern matching: console.log(
area({ size: 10 }),
area({ width: 10, height: 11 }),
area({ radius: 1 / Math.sqrt(Math.PI) }))
// => 100, 110, 1 This works in JS, so the goal in TypeScript should be to support this with strict type checking and good inference. |
Yeah, jumping through hoops just to emulate pattern matching for the one feature that is already very well supported by the compiler (discriminated unions + control flow analysis) is pointless as anything but an exercice. Plus, you lose the free exhaustivity check! Without compiler support, it's not worth it. Will wait for that TC39 pattern matching proposal to go up in its stages, or for macros :D |
I’m not sure if you’re referring to Unionize, but it’s not “emulating” pattern matching; it’s using every bit of the existing “very well supported” facility that the compiler provides. And it does exhaustivity checking! |
@pelotom Yes I was referring to it indirectly, along with a few other implementations I've seen. If you're supporting exhaustivity, I really need to have a look... But you don't have the order-dependent + fall-through characteristic of general purpose pattern matching, right? |
Correct, it supports the special case where you are discriminating by a unique tag and providing one case per tag, or a catchall default case to handle the leftovers (in which case you lose exhaustivity checking obv). |
@AlexGalays I want to jump in and comment on something. I never saw @pelotom's unionize as a replacement for full blown pattern matching capability. But there a subset of pattern matching that |
Take a look at @ahejlsberg's take on this from Microsoft Build 2017. From around 46:42, he uses the |
A second plug: Runtypes now supports pattern matching. Here's @masaeedu's shapes example adapted to it: |
I know this issue has kind of gone stagnant, but I would love to show my support for this feature. Exhaustive pattern matching combined with algebraic data types is my favorite feature of F#. I don't know the specifics of implementing a feature like this in the current code base, but the syntax is so close to being what I want to work with already. I want to write code like this:
The lightweight types and discriminated union provide a clear, concise, and powerful domain model, and the potential exhaustive pattern matching would round everything out by enforcing that domain model on the developer via the compiler. It's a system that really works very well in the F# world and it would be amazing to see something similar in TS. It's clearly possible to transpile to javascript that enforces this, as evidenced by the existence of Fable. For now, I'm stuck using Fable basically for this functionality alone. Unfortunately, the developer experience is not as good as TS, and TS retains some of that Javascript flexibility (which I'm a fan of). In my mind, this feature alone would be enough for me to make the switch, and I doubt that I'm the only one. Thanks for the consideration. |
What about this ECMAScript proposal? There's a couple implementations of it and the syntax looks like it would fit well. I think this shows that it's at least possible. |
Very likely there's no movement on this issue until that proposal reaches stage 3. Edit: I don't much like it either folks, but check TypeScript's project goals and its past behaviour, there's zero chance of a massive break from the standards on this, and implementing a standard too early has already backfired once. |
Is there any proposal\issue on ternary operator, like return anyArray.find({condition}) let item ? computation_over(item.{prop}) : undefined? |
Pursuant to our goals of a) not adding type-directed emit and b) not getting ahead of TC39 on runtime features like this, I'm going to close this as out of scope for the time being. When https://github.com/tc39/proposal-pattern-matching gets to Stage 3 we'll implement this ASAP, but no sooner. Further discussion on how that feature should work belongs entirely in the proposal repo, so I'll be locking this to better channel discussion to the correct place. Thanks! |
I've seen some short notions about pattern matching on code plex, but no serious discussions around it. As I'm a big fan of it, I've written up some small examples on how I think pattern matching could look in TypeScript.
This is by far no complete specification, just some ideas that I want to bring forward and have a little discussion around.
Function Overloading
Match
statementThe text was updated successfully, but these errors were encountered: