在像OCaml这样的功能语言中,我们有模式匹配。例如,我想记录用户'在我的网站上的行动。一个动作可以是1)访问网页,2)删除项目,3)检查另一个用户的个人资料等。在OCaml中,我们可以写如下内容:
type Action =
| VisitPage of string (* www.myweb.com/help *)
| DeletePost of int (* an integer post id *)
| ViewUser of string (* a username *)
但是,我不确定如何在JavaScript中定义此Action
。我能想象的一种方式是
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" }
但是这种结构并不表示pageVisited
,postDeleted
和userViewed
是排他性的。
有人可以在JavaScript中提出更好的此类型表示吗?
是否有一种在JavaScript或TypeScript中进行模式匹配的常用方法?
答案 0 :(得分:7)
你想要一个discriminated union,TypeScript支持它通过添加一个具有不同字符串文字值的公共属性,如下所示:
type VisitPage = { type: 'VisitPage', pageVisited: string }
type DeletePost = { type: 'DeletePost', postDeleted: number }
type ViewUser = { type: 'ViewUser', userViewed: string }
type Action = VisitPage | DeletePost | ViewUser
Action
属性区分type
类型,当您检查Action
属性时,TypeScript将自动执行控制流分析以缩小type
。这就是模式匹配的方法:
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');
}
}
请注意,如果您愿意,可以添加详尽的检查,因此,如果您要向Action
联合添加其他类型,则上述代码将为您提供编译时警告。
希望有所帮助;祝你好运!
答案 1 :(得分:2)
函数式编程中的类型可以用类来模仿:
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);

用于模式匹配:
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);

答案 2 :(得分:2)
模式匹配的面向对象化身是访问者模式。我已经使用"匹配"而不是"访问"在下面的片段中强调通信。
// 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);

经过一些重构后,你可以获得类似的东西,它看起来非常接近OCaml提供的东西:
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);

或者
action1.match({
VisitPage(url) { console.log(url) },
DeletePost(id) { console.log(id) },
ViewUser(username) { console.log(username) },
});
甚至(使用ES2015匿名类):
action1.match(class {
static VisitPage(url) { console.log(url) }
static DeletePost(id) { console.log(id) }
static ViewUser(username) { console.log(username) }
});
优于OCaml的优点是匹配块是第一类,就像函数一样。您可以将其存储在变量中,将其传递给函数并从函数中返回。
为了消除变体名称中的代码重复,我们可以设计一个帮助器:
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) },
});
答案 3 :(得分:1)
由于它们是正交的,因此它们不必共享任何结构。
如果你仍然喜欢“共同结构”的概念,你可以使用类提到的@Derek朕会功夫,或者使用一些常见的结构,例如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' }