How To Represent Algebraic Data Types And Pattern Matching In Javascript
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"