Skip to content Skip to sidebar Skip to footer

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

Solution 1:

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:

functiondoSomething(action: Action) {
  switch (action.type) {
    case'VisitPage':
      // action is narrowed to VisitPageconsole.log(action.pageVisited); //okaybreak;
    case'DeletePost':
      // action is narrowed to DeletePostconsole.log(action.postDeleted); //okaybreak;
    case'ViewUser':
      // action is narrowed to ViewUserconsole.log(action.userViewed); //okaybreak;
    default:
      // action is narrowed to never (bottom), // or the following line will errorconstexhausivenessWitness: never = action; //okaythrownewError('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!

Solution 2:

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

classAction {}
classVisitPageextendsAction {
    constructor(pageUrl){
        super();
        this.pageUrl = pageUrl;
    }
}
classViewUserextendsAction {
    constructor(userName){
        super();
        this.userName = userName;
    }
}

var myAction = newVisitPage("http://www.google.com");
console.log(myAction instanceofAction);
console.log(myAction.pageUrl);

For pattern matching:

classAction {}
classVisitPageextendsAction {
    constructor(pageUrl){
        super();
        this.pageUrl = pageUrl;
    }
}
classViewUserextendsAction {
    constructor(userName){
        super();
        this.userName = userName;
    }
}

functioncomputeStuff(action){
    switch(action.constructor){
        caseVisitPage:
            console.log(action.pageUrl); break;
        caseViewUser:
            console.log(action.userName); break;
        default:
            thrownewTypeError("Wrong type");
    }
}

var action = newViewUser("user_name");
var result = computeStuff(action);

Solution 3:

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:

functionVariant(name) {
  returnfunction (...args) {
    return { match(matcher) { return matcher[name](...args); } };
  };
}
    
constAction = {
  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:

functionVariants(...names) {
  constvariant = (name) => (...args) => ({
    match(matcher) { return matcher[name](...args) }
  });
  const variants = names.map(name => ({ [name]: variant(name) }));
  returnObject.assign({}, ...variants);
}

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

Solution 4:

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

Post a Comment for "How To Represent Algebraic Data Types And Pattern Matching In Javascript"