5

In functional language like OCaml, we have pattern matching. For example, I want to log users' actions on my website. An action could be 1) visiting a web page, 2) deleting an item, 3) checking the profile of another user, etc. In OCaml, we can write something as follows:

type Action = 
  | VisitPage of string (* www.myweb.com/help *)
  | DeletePost of int (* an integer post id *)
  | ViewUser of string (* a username *)

However, I am not sure how to define this Action in JavaScript. One way I could imagine is

var action_1 = { pageVisited: "www.myweb.com/help", postDeleted: null, userViewed: null }
var action_2 = { pageVisited: null, postDeleted: 12345, userViewed: null }
var action_3 = { pageVisited: null, postDeleted: null, userViewed: "SoftTimur" }

But this structure does not express that pageVisited, postDeleted and userViewed are exclusive among them.

Could anyone propose a better representation of this type in JavaScript?

Is there a common way to do pattern matching in JavaScript or TypeScript?

Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
SoftTimur
  • 5,630
  • 38
  • 140
  • 292

4 Answers4

9

You want a discriminated union, which TypeScript supports by adding a common property with different string literal values, like so:

type VisitPage = { type: 'VisitPage', pageVisited: string }
type DeletePost = { type: 'DeletePost', postDeleted: number }
type ViewUser = { type: 'ViewUser', userViewed: string }

type Action = VisitPage | DeletePost | ViewUser

The Action type is discriminated by the type property, and TypeScript will automatically perform control flow analysis to narrow an Action when you inspect its type property. This is how you get pattern matching:

function doSomething(action: Action) {
  switch (action.type) {
    case 'VisitPage':
      // action is narrowed to VisitPage
      console.log(action.pageVisited); //okay
      break;
    case 'DeletePost':
      // action is narrowed to DeletePost
      console.log(action.postDeleted); //okay
      break;
    case 'ViewUser':
      // action is narrowed to ViewUser
      console.log(action.userViewed); //okay
      break;
    default:
      // action is narrowed to never (bottom), 
      // or the following line will error
      const exhausivenessWitness: never = action; //okay
      throw new Error('not exhaustive');
  }
}

Note that you can add an exhaustiveness check, if you wish, so if you ever add another type to the Action union, code like the above will give you a compile-time warning.

Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Good to know that TypeScript supports this... So JavaScript does not support this way at all? – SoftTimur Nov 29 '17 at 01:39
  • 1
    If you transpile the above code, the resulting JavaScript will not protect you against someone passing in a malformed `Action` type at runtime. To guarantee type safety at runtime without TypeScript, you can use ES2015 classes as @Derek 朕會功夫 suggests (or the equivalent ES5 constructor code). – jcalz Nov 29 '17 at 02:03
  • This is the right approach. Using a class won't guarantee anything since prototype chains are mutable at runtime. – Aluan Haddad Nov 29 '17 at 03:38
  • You can use a runtime type check system like [sanctuary-def](https://github.com/sanctuary-js/sanctuary-def) used by libraries like Sanctuary.js and Fluture. It's very resource expensive but can be turned off in production and used only during development. – Fabiano Taioli Oct 24 '20 at 16:28
6

A type in functional programming can be mimicked with a class:

class Action {}
class VisitPage extends Action {
    constructor(pageUrl){
        super();
        this.pageUrl = pageUrl;
    }
}
class ViewUser extends Action {
    constructor(userName){
        super();
        this.userName = userName;
    }
}

var myAction = new VisitPage("http://www.google.com");
console.log(myAction instanceof Action);
console.log(myAction.pageUrl);

For pattern matching:

class Action {}
class VisitPage extends Action {
    constructor(pageUrl){
        super();
        this.pageUrl = pageUrl;
    }
}
class ViewUser extends Action {
    constructor(userName){
        super();
        this.userName = userName;
    }
}

function computeStuff(action){
    switch(action.constructor){
        case VisitPage:
            console.log(action.pageUrl); break;
        case ViewUser:
            console.log(action.userName); break;
        default:
            throw new TypeError("Wrong type");
    }
}

var action = new ViewUser("user_name");
var result = computeStuff(action);
Derek 朕會功夫
  • 92,235
  • 44
  • 185
  • 247
4

Visitor Pattern

The object-oriented incarnation of pattern matching is the visitor pattern. I've used "match", instead of "visit" in the following snippet to emphasize the correspondence.

// OCaml: `let action1 = VisitPage "www.myweb.com/help"`
const action1 = {
  match: function (matcher) {
    matcher.visitPage('www.myweb.com/help');
  }
};

// OCaml: `let action2 = DeletePost 12345`
const action2 = {
  match: function (matcher) {
    matcher.deletePost(12345);
  }
};

// OCaml: `let action2 = ViewUser SoftTimur`
const action3 = {
  match: function (matcher) {
    matcher.viewUser('SoftTimur');
  }
};

// These correspond to a `match ... with` construct in OCaml.
const consoleMatcher = {
  visitPage: function (url) {
    console.log(url);
  },

  deletePost: function (id) {
    console.log(id);
  },

  viewUser: function (username) {
    console.log(username);
  }
};

action1.match(consoleMatcher);
action2.match(consoleMatcher);
action3.match(consoleMatcher);

After some refactoring, you can obtain something like this, which looks pretty close to what OCaml offers:

function Variant(name) {
  return function (...args) {
    return { match(matcher) { return matcher[name](...args); } };
  };
}
    
const Action = {
  VisitPage: Variant('VisitPage'),
  DeletePost: Variant('DeletePost'),
  ViewUser: Variant('ViewUser'),
};

const action1 = Action.VisitPage('www.myweb.com/help');
const action2 = Action.DeletePost(12345);
const action3 = Action.ViewUser('SoftTimur');

const consoleMatcher = {
  VisitPage(url) { console.log(url) },
  DeletePost(id) { console.log(id) },
  ViewUser(username) { console.log(username) },
};

action1.match(consoleMatcher);
action2.match(consoleMatcher);
action3.match(consoleMatcher);

Or

action1.match({
  VisitPage(url) { console.log(url) },
  DeletePost(id) { console.log(id) },
  ViewUser(username) { console.log(username) },
});

Or even (using ES2015 anonymous classes):

action1.match(class {
  static VisitPage(url) { console.log(url) }
  static DeletePost(id) { console.log(id) }
  static ViewUser(username) { console.log(username) }
});

The advantage over OCaml is that the match block is first class, just like functions. You can store it in variables, pass it to functions and return it from functions.

To eliminate the code duplication in variant names, we can devise a helper:

function Variants(...names) {
  const variant = (name) => (...args) => ({
    match(matcher) { return matcher[name](...args) }
  });
  const variants = names.map(name => ({ [name]: variant(name) }));
  return Object.assign({}, ...variants);
}

const Action = Variants('VisitPage', 'DeletePost', 'ViewUser');

const action1 = Action.VisitPage('www.myweb.com/help');

action1.match({
  VisitPage(url) { console.log(url) },
  DeletePost(id) { console.log(id) },
  ViewUser(username) { console.log(username) },
});
Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
1

Since they are orthogonal, they don't have to share any structure.

If you still like the concept of "common structure" you can use class as @Derek 朕會功夫 mentioned, or use some common structure such as https://github.com/acdlite/flux-standard-action

const visitPage = { type: 'visit_page', payload: 'www.myweb.com/help' }
const deletePose = { type: 'delete_post', payload: 12345 }
const viewUser = { type: 'view_user', payload: 'SoftTimur' }
unional
  • 14,651
  • 5
  • 32
  • 56