Skip to content
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

Open
dignifiedquire opened this issue Jul 21, 2014 · 63 comments

Comments

Projects
None yet
@dignifiedquire
Copy link

commented Jul 21, 2014

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

// Pattern matching on functions

function pickCard {
  (x: {suit: string; card: number; }[]): number {

    var pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;  

  }
  (x: number): {suit: string; card: number; } {

    var pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };

  }
} 
// Translation of functions.ts using sparkler
// https://github.com/natefaubion/sparkler

function pickCard(x) {
  [...{ suit @ String, card @ number }] =>  {
    var pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;  
  }
  x @ Number => {
    var pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
} 

Match statement

// Pattern matching using match

function whoami (x: any): string {

  match x {
    case { name: string, type: 'person' }: 
        return 'Person: ' + name;

    case { name: string, type: 'animal' }: 
        return 'Animal: ' + name;

    default: 
        return 'Unkown you';
  }
} 
// Pattern matching using match translated

function whoami (x) {

  match x {
    case { name @ String, type: 'person' }: 
        return 'Person: ' + name;

    case { name @ String, type: 'animal' }: 
        return 'Animal: ' + name;

    default: 
        return 'Unkown you';
  }

} 
@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented Jul 21, 2014

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);
@justinmchase

This comment has been minimized.

Copy link

commented Jul 22, 2014

@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
http://www.tinlizzie.org/ometa-js/#Typechecker

@AlexGalays

This comment has been minimized.

Copy link

commented Mar 10, 2015

"We've typically avoided features that require type information at runtime"

Will that change with the accommodating of the angular2 reflection?

@mhegazy

This comment has been minimized.

Copy link

commented Mar 10, 2015

@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.

@refi64

This comment has been minimized.

Copy link

commented Aug 24, 2015

I have to say, something like this would be insanely nice. What are the chances of this happening?

@SamuelMarks

This comment has been minimized.

Copy link

commented Sep 21, 2015

👍

3 similar comments
@dev-tim

This comment has been minimized.

Copy link

commented Oct 5, 2015

👍

@svanderbleek

This comment has been minimized.

Copy link

commented Oct 9, 2015

👍

@slavah

This comment has been minimized.

Copy link

commented Oct 23, 2015

👍

@tejacques

This comment has been minimized.

Copy link

commented Dec 6, 2015

Just wanted to point out a possible alternative that may be available with --noImplicitReturns with the modification mentioned here #5916.

This gets you code that looks like this, and requires no changes to TypeScript whatsoever
interface 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';
    }
}

--noImplicitReturns gives us full spectrum matching in a very conventionally JavaScript way. We can now make a pretty trivial helper function match:

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 sugar

Make match a built-in construct which would look like this:

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 if(isType(value)) return typeFn(value) statement, for each function passed in. For primitive types, it uses: typeof (value) === '<primitive>', for "tagged" or disjoint unions specified with type literals, the field containing the literal type could be checked, and for any other type, it checks to see if a function is<Type> exists in scope.

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 isSuit or isRank type guard exists in scope, it would give a compiler error. It could also conceivably give a compiler error in the case of overlapping type literals, since all their values are known at compile time.

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);
}
@ghost

This comment has been minimized.

Copy link

commented Feb 11, 2016

👍

But why not erlang style function guards? The syntax proposed above is a little funky.

function animal(species: Cat) {
    species.meow();
}

function animal(species: Dog) {
    species.bark();
}

becomes:

function animal(species: Cat | Dog) {
    if (species instanceof Cat) {
        species.meow();
    } else if (species instanceof Dog) {
        species.bark();
    }
}

or checking values:

function animal(alive: true) : string {
    return "Is alive";
}

function animal(alive: false) : string {
    return "Is not alive";
}
@felixSchl

This comment has been minimized.

Copy link

commented Feb 11, 2016

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. case x:xs | x == 0 to check if this is a list where the first element is a 0.

Your suggestions reminds me of multi-methods in lisp or function lookup based on arity like in elixir.

@tejacques

This comment has been minimized.

Copy link

commented Feb 11, 2016

@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.

@ghost

This comment has been minimized.

Copy link

commented Feb 11, 2016

@felixSchl

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:

trait Multiply {
    fn multiply(self, i32) -> i32;
}

impl Multiply for String {
    fn multiply(self, n: i32) -> i32 {
        self.parse::<i32>().unwrap() * n
    }
}

impl Multiply for i32 {
    fn multiply(self, n: i32) -> i32 {
        self * n
    }
}

fn test<T: Multiply>(x: T, n: i32) -> i32 {
    x.multiply(5)
}

fn main() {
    println!("{}", test("5".to_owned(), 5)); //25
    println!("{}", test(5, 5)); // 25
}

Rust is notoriously verbose. I'd like to just do this:

fn test(x: String, n: i32) -> i32 {
    self.parse::<i32>().unwrap() * n
}

fn test(x: i32, n: i32) -> i32 {
    x * n
}

But I can't...

@tejacques

You're right, I had forgotten about interfaces (I have been switching between languages). Do you have any alternative solutions?

@felixSchl

This comment has been minimized.

Copy link

commented Feb 11, 2016

@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.

@ghost

This comment has been minimized.

Copy link

commented Feb 11, 2016

@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.

@alrz

This comment has been minimized.

Copy link

commented Feb 11, 2016

why not extending switch statement, just like C#.

@felixSchl

This comment has been minimized.

Copy link

commented Feb 11, 2016

@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 if as an example. Here's some contrieved purescript:

let y = if x == 0 then "foo" else "bar"

which you would need to do imperatively using if statements:

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";
@tejacques

This comment has been minimized.

Copy link

commented Feb 11, 2016

@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.

@gneuvill

This comment has been minimized.

Copy link

commented May 25, 2016

👍

@baio

This comment has been minimized.

Copy link

commented May 29, 2016

You could do redundant pattern matching right now

class Some {
    constructor(public val: string) {       
    }
}

class None {
}


type Maybe = Some | None;

var a = new Some("val");

if (a instanceof Some) {
    console.log(a.val);
}

But some syntax sugar would be appreciated.

@ablankholm

This comment has been minimized.

Copy link

commented Jun 30, 2016

Any reason https://github.com/bendetat/SimplicityJS wouldn't work with TS?

@schickling

This comment has been minimized.

Copy link

commented Aug 4, 2016

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!

@mmc41

This comment has been minimized.

Copy link

commented Sep 28, 2016

@tejacques Do you have an update of your interesting example for typescript 2.0 ?

@aesteve

This comment has been minimized.

Copy link

commented Oct 18, 2016

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 reducer for a certain type of action. Your whole chain is now strongly typed.

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) :

type alias Todo = {id: String, items: Array[String]}
val CreateTodo = Action(Todo)
val RemoveTodo = Action({id: String})
type alias TodoAction = CreateTodo | RemoveTodo
type alias SomeState = Map[Todo]
function updateState(action: TodoAction, state: SomeState) {
  match action.type {
    case CreateTodo newTodo => // here the compiler knows that newTodo is {id: String, items: Array[String]})
    case UpdateTodo {id} => state.remove(id)
    // => no default, the compiler wouldn't compile if I forgot about some case
  }

} 

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 : payload.action doesn't have the right type)

@tekacs

This comment has been minimized.

Copy link

commented Oct 18, 2016

@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
@xogeny

This comment has been minimized.

Copy link

commented Nov 9, 2016

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.

class Foo {
  public name: string;
}

class Bar {
  public title: string;
}

type Stringable = A | B;

function stringOf(a: Stringable): string {
  if (a instanceof Foo) return a.name;
  return a.title;
}

That's great. But something like this would just be easier to read:

function stringOf(a: Stringable): string {
  a match {
     case Foo: return a.name;
     case Bar: return a.title;
 }
}

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 instanceof is using). Of course, instrumenting this with destructuring and guards would be really cool too! e.g.,

function lengthOf(a: Stringable): number {
  a match {
    case Foo({ name }) if name: return name.length;
    case Bar({ title }) if title: return title.length;
    default: return 0;
  }
}

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:

function lengthOf(a: Stringable): number {
  if (a instanceof Foo && a.name) return a.name.length;
  else if (a instanceof Bar && b.title) return a.title.length;
  else return 0;
}

...with the same possibilities for detection mutual exhaustion.

Or am I missing something?

@jvelezc

This comment has been minimized.

Copy link

commented Jun 10, 2017

How about exactly like c# 7.1

if(v is int i)
{
    add(i,1);
}

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

As documented here:https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/

@SamuelMarks

This comment has been minimized.

Copy link

commented Jun 12, 2017

@jvelezc Scala has a much nicer syntax:

def goIdle(device: Device) = device match {
  case p: Phone => p.screenOff
  case c: Computer => c.screenSaverOn
}
val y = x match {
  case Pair(i, s) => Pair(s + i, i * i)
}
(x, y) => (x, y) match {
  case (a, (b, c)) => a + b * c
}

But yeah, the thing with pattern matching is that it differs from ECMAScript standards and proposed standards. So we'll have to be like other exceptions in TypeScript such as interface if we want proper pattern matching support built-in.

@Dessix

This comment has been minimized.

Copy link
Member

commented Jun 13, 2017

It'd be lovely if we could make Scala's pattern matching syntax more widely accepted this way. I am certainly a fan of moving it in that direction. The only big change is that we'd be diverging not only from ES but also the Typescript idea that removing all annotations would produce the same runtime behavior on valid code. Being that we already have to have a compile phase with TS, I don't see a problem with it producing more useful code.

@masaeedu

This comment has been minimized.

Copy link
Contributor

commented Jun 24, 2017

While reflecting on runtime types may not fit well with the zen of TypeScript, matching on Haskell style labeled unions should be possible. I was trying to get this to work correctly, but there's some issues with inference on unions (#2264) that makes it annoying to use:

type Case<T extends string> = { _: T }
type Maybe<T> = { _: "just", value: T } | { _: "none" }

function match<
    C1 extends string, U1 extends Case<C1>,
    C2 extends string, U2 extends Case<C2>>(
    data: U1 | U2,
    c1: C1, p1: (data: U1) => any,
    c2: C2, p2: (data: U2) => any): any
/* more overloads... */
function match(...args: any[]): any
{ /* ... */ }

// Almost, but x is inferred as just Case<"just"> instead of { _: "just", value: T } :(
match(
    <Maybe<number>>undefined,
    "just", x => console.log(x),
    "none", () => console.log('nope'))

// Whoops, this is a type error
match(
    <Maybe<number>>undefined,
    "wrong", x => console.log('nope'),
    "just", x => console.log('foo'))
@danielrasmuson

This comment has been minimized.

Copy link

commented Jul 9, 2017

FYI. tc39 recently released a stage 0 proposal to add pattern matching as an official javascript feature https://github.com/tc39/proposal-pattern-matching

@lostintime

This comment has been minimized.

Copy link

commented Sep 8, 2017

Recently published a tiny library: https://www.npmjs.com/package/typematcher

import {match, caseId, caseThrow, isEither, isString, isValue, isNumber, isOptional} from 'typematcher'

enum Gender {
  Male = 'M',
  Female = 'F'
}

type User = {
  name: string,
  gender: Gender,
  age: number
  address?: string
}

const user: User = match({})(
  caseId(
    hasFields({
      name: isString,
      gender: isEither(isValue(Gender.Male), isValue(Gender.Female)),
      age: isNumber,
      address: isOptional(isString)
    })
  ),
  caseWhen(isNumber)(n => {
    throw new Error("Number not an user");
  }),
  caseThrow(new Error("Invalid user object"))
);

Sources: https://github.com/lostintime/node-typematcher
Short intro: https://lostintimedev.com/2017/09/06/typematcher-pattern-matching-library-for-typescript.html

+1 for native scala-like syntax

@gbegher

This comment has been minimized.

Copy link

commented Oct 5, 2017

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'
})
@laser

This comment has been minimized.

Copy link

commented Nov 6, 2017

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 Shape.

@xogeny

This comment has been minimized.

Copy link

commented Nov 6, 2017

I've used a similar pattern. I'm not sure the problem is with subclassing (implementing, really) Shape. After all, they will have to provide an implementation of match, so that is on them.

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.

@xogeny

This comment has been minimized.

Copy link

commented Nov 6, 2017

Ah...I think I see what you mean by extending Shape. I suppose you are referring to the problem of what to do if they want to add another possibility to the list of possibilities (i.e., growing it from 3 to 4 to ...). In Scala, this is solved by sealing the class (thus freezing the possible subtypes). We don't have that in TypeScript of course.

If you have an open ended set of possibilities, you are probably better of simply defining a perimeter method. If it is a closed set of possibilities (and you don't have the option to "seal" them as in Scala), I think the approach you showed works just fine because the match method effectively closes the set of possibilities for you.

If you really wanted to, I suppose you could do something like this:

interface ShapeHandler<T> {
    square: (x: Square) => T;
    rectangle: (y: Rectangle) => T;
    circle: (z: Circle) => T;
}
interface Shape {
    match<T>(handler: ShapeHandler<T>): T;
}

class Square implements Shape {
    constructor(public size: number) { }
    match<T>(handler: ShapeHandler<T>): T { return handler.square(this); }
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) { };
    match<T>(handler: ShapeHandler<T>): T { return handler.rectangle(this); }
}

class Circle implements Shape {
    constructor(public radius: number) { };
    match<T>(handler: ShapeHandler<T>): T { return handler.circle(this); }
}

let x: Shape = new Square(10);

console.log(x.match({
    square: (sqr) => { return sqr.size * sqr.size },
    rectangle: (rec) => { return rec.width * rec.height },
    circle: (cir) => { return Math.PI * cir.radius ** 2 }
}));

interface ExtendedShapeHandler<T> extends ShapeHandler<T> {
    triangle: (t: Triangle) => T;
}

class Triangle implements Shape {
    constructor(public a: number, public b: number, public c: number) { };
    match<T>(handler: ExtendedShapeHandler<T>) { return handler.triangle(this) }
}

console.log(x.match({
    square: (sqr) => { return sqr.size * sqr.size },
    rectangle: (rec) => { return rec.width * rec.height },
    circle: (cir) => { return Math.PI * cir.radius ** 2 },
    triangle: (tri: Triangle) => { return tri.a + tri.b + tri.c },
} as ShapeHandler<number>));

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 ShapeHandler and just seems the triangle bit as "extra" (although the Triangle won't see it that way). But this is still unsafe because we have no way to ensure that an extended shape will get passed an ExtendedShapeHandler.

@AlexGalays

This comment has been minimized.

Copy link

commented Nov 6, 2017

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)

@goodmind

This comment has been minimized.

Copy link

commented Nov 6, 2017

How would you do proper pattern matching in TypeScript if types are erased at runtime? Isn't this pointless

@AlexGalays

This comment has been minimized.

Copy link

commented Nov 6, 2017

@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 { x, ... y }: /* match an object with x, stuff any remaining properties in y */ would have to raise compilation errors if the compiler can't know for a fact what the type for y is, when in strict mode.

@goodmind

This comment has been minimized.

Copy link

commented Nov 7, 2017

@AlexGalays It's stage 0 tho

@xogeny

This comment has been minimized.

Copy link

commented Nov 7, 2017

@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.

@laser

This comment has been minimized.

Copy link

commented Nov 7, 2017

A discriminated union-based approach may also be an option for you. As per the docs:

type aliases cannot be extended or implemented from (nor can they extend/implement other types)

...which addresses the issue I mentioned earlier re: preventing programmers from adding new implementations of Shape.

Note that exhaustiveness in the switch/case used in the match method is guaranteed by the --strictNullChecks compiler flag plus a return type of T.

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 }));
@masaeedu

This comment has been minimized.

Copy link
Contributor

commented Nov 7, 2017

@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 match that makes this all work, we're not quite there yet. I've tried to point out the missing features in #19800.

@pelotom

This comment has been minimized.

Copy link

commented Nov 8, 2017

Just saw this issue and thought I'd put in a plug:

https://github.com/pelotom/unionize

You can have pattern matching today!

@xogeny

This comment has been minimized.

Copy link

commented Nov 8, 2017

@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!

@masaeedu

This comment has been minimized.

Copy link
Contributor

commented Nov 8, 2017

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 x:xs in Haskell should map to matching over ([x, ...y]) => in JS. This isn't possible if pattern matching is restricted to a single discriminator property.

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.

@AlexGalays

This comment has been minimized.

Copy link

commented Nov 9, 2017

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

@pelotom

This comment has been minimized.

Copy link

commented Nov 9, 2017

@AlexGalays

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!

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!

@AlexGalays

This comment has been minimized.

Copy link

commented Nov 9, 2017

@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?

@pelotom

This comment has been minimized.

Copy link

commented Nov 9, 2017

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).

@xogeny

This comment has been minimized.

Copy link

commented Nov 9, 2017

@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 unionize does an excellent job of representing while eliminating lots of boilerplate. For example, I submitted a PR to the bombadil project and I can imagine that a bunch of the boilerplate there could be cleaned up very nicely with unionize. So I'd argue that while it isn't a replacement for pattern matching support at the language level, I think it is far from "pointless" because it is a huge improvement for some very common cases.

@LayZeeDK

This comment has been minimized.

Copy link

commented Jan 13, 2018

Take a look at @ahejlsberg's take on this from Microsoft Build 2017. From around 46:42, he uses the never type to ensure that all the possible known shapes have been matched in a regular switch-case statement.

This is a lot like what @tekacs proposed.

@pelotom

This comment has been minimized.

@dough654

This comment has been minimized.

Copy link

commented Feb 15, 2019

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:

type CreditCard = {
    number: number;
    expDate: string;
    cvc: number;
}

type Check = {
    routingNumber: number
    accountNumber: number
}

type PayPal = {
    emailAddress: string
}

type PaymentMethod = CreditCard | Check | PayPal; 

type Payment = {
    Method: PaymentMethod; 
    Amount: number
}

function processPayment(payment: Payment) {
    match (payment) {
        CreditCard: cc => ...//process credit card
        Check: check => ...//process check payment
        // -> Would throw an compile time warning as not all cases have been handled
    }
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.