繁体   English   中英

如何在 JavaScript 中表示代数数据类型和模式匹配

[英]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" }

但是这个结构并不表示pageVisitedpostDeleteduserViewed是排他性的。

有人能在 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.

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