[英]How to represent algebraic data types and pattern matching in JavaScript
在像 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 中进行模式匹配的通用方法?
您需要一个可区分的 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
属性区分,当您检查其type
属性时,TypeScript 将自动执行控制流分析以缩小Action
。 这是您获得模式匹配的方式:
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
联合添加另一种类型,上述代码将给您一个编译时警告。
希望有所帮助; 祝你好运!
函数式编程中的一个类型可以用一个类来模拟:
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);
模式匹配的面向对象化身是访问者模式。 我在下面的代码片段中使用了“匹配”而不是“访问”来强调对应关系。
// 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) },
});
由于它们是正交的,因此它们不必共享任何结构。
如果你仍然喜欢“通用结构”的概念,你可以使用@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' }
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.