如何在JavaScript

时间:2017-11-29 00:48:59

标签: javascript typescript object pattern-matching ocaml

在像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中进行模式匹配的常用方法?

4 个答案:

答案 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' }