如何在没有模式匹配的情况下用动态类型语言实现和类型

How to implement sum types in dynamically typed languages without pattern matching

我对 Haskell 的求和类型只有一个理论概念。然而我感觉到它们在 Haskell 中确实很重要,并且从根本上改变了数据建模的方式。因为我相信它们在动态类型语言中也很有用,所以我尝试在 Javascript 中实现一个合理的近似(我对 Haskell 的了解很肤浅)。

这是一个或多或少有用的 Name 求和类型示例,它能够处理各种名称格式。我知道 Haskell 区分类型构造函数和数据构造函数,并且可能有充分的理由进行这种区分。不过,我猜想把这个概念映射到Javascript是不可能的。并且模式都不匹配。

无论如何,我的实际问题是:以下实现是否体现了求和类型的性质及其在 Haskell 中的应用方式?

请注意:我不确定是否欢迎在SO上提出这种跨语言问题。如果我应该避开它们,请告诉我。

// auxiliary functions

const A = f => x => f(x);
const show = api => api.show;

// the type constructor

const Name = (...xs) => A(({length: len}) => {
  switch (len) {
    case 1: {
      let [{length: len}] = xs; // no pattern matching but destructuring

      if (len > 1) { // no Char type in JS
        return k => k({show: () => xs[0]}); // returns the API
      }

      break;
    }

    case 2: {
      let [{length: len}, {length: len2}] = xs;

      if (len > 1 && len2 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]}`});
      }

      if (len === 1 && len2 > 1) {
        return k => k({show: () => `${xs[0]}. ${xs[1]}`});
      }

      break;
    }

    case 3: {
      let [{length: len}, {length: len2}, {length: len3}] = xs;

      if (len > 1 && len2 > 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]} ${xs[2]}`});
      }

      if (len > 1 && len2 === 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]}. ${xs[2]}`});
      }

      if (len === 1 && len2 === 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]}. ${xs[1]}. ${xs[2]}`});
      }
    }

    default: throw new TypeError();
  }
}) (xs);

// run

console.log(Name("Kerouac") (show) ());
console.log(Name("Hans", "Hölzel") (show) ());
console.log(Name("H", "Curry") (show) ());
console.log(Name("Jean", "Luc", "Godard") (show) ());
console.log(Name("William", "S", "Burroughs") (show) ());
console.log(Name("E", "W", "Dijkstra") (show) ());

[编辑]

抱歉,我应该提供一些 Haskell 代码:

type FirstName = String
type LastName = String
type MiddleName = String

data Name = FullName FirstName LastName
  | NameWithMiddle FirstName MiddleName LastName
  | NameWithMiddleInitial FirstName Char LastName
  | TwoInitialsWithLast Char Char LastName
  | OneInitialWithLast Char LastName
  | LastNameOnly LastName

不过我不确定这是否有效。

我想我的方法的问题是我尝试实现类型构造函数 Name,而我应该实现值构造函数,对吗?

我认为您的编码有点过于复杂,因为它不仅涉及数据类型本身,还涉及 show 操作。

sum 类型的本质是它们为您提供了一种方法来创建表示一个或多个案例的值,然后使用模式匹配编写处理代码。您的示例的一个稍微简单的版本只有全名或简称:

data Name = 
    FullName String String
  | NickName String

然后你可以写一个函数来处理一个名字,分别处理这两种情况:

case name of
  FullName first last -> "Full name: " ++ first ++ " " ++ last
  NickName nick -> "Nick name: " ++ nick

关键思想是,当你有一个名字时,你可以检测它是两种情况中的哪一种,并编写代码来分别处理这两种情况。在 JavaScript 中,您可以通过在您的值中包含某种标签来做到这一点:

function FullName(first, last) { 
  return { Tag: "FullName", Values: [first, last] };
}
function NickName(nick) { 
  return { "Tag": "NickName", Values: [nick] };
}

现在您可以在 Tag 上使用 switch 编写类似于模式匹配的代码:

switch(name.Tag) { 
  case "FullName": 
    let [first, last] = name.Values;
    return "Full name: " + first + " " + last;
  case "NickName":
    let [nick] = name.Values
    "Nick name: " + nick;
}

但是,如果没有语言支持,您将失去许多不错的功能:

  • 提取值时没有检查。如果修改类型并添加更多字段,则模式匹配将开始失败。
  • 不会检查您是否涵盖了所有情况 - 当您添加另一种名称时,switch 语句将不会涵盖那种情况
  • 没有检查标签名称。很容易打错字

所以,你当然可以在 JavaScript 中使用这种编码,但它并没有直接支持求和类型的语言那么多。如果有更惯用的 JavaScript 解决方案来解决您遇到的问题,它可能会更好。