簡體   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