简体   繁体   中英

How to represent algebraic data types and pattern matching in JavaScript

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?

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!

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

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) },
});

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' }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM