总和类型更好的JS案例

改善语义和正确性·通过Redux State进行说明

抽象

JavaScript具有内置的原子标量类型,例如数字和布尔值。 它还可以通过数组表示复合产品类型 ,也可以通过对象表示记录类型 。 但是,它缺少对不相交和类型的立即解决方案 总和类型(又名带标签或有区别的联合)是其他语言中的常用工具; 它们允许值成为一组显式情况中的一种,并具有轻松,安全的标识和数据提取功能。 本文使用Redux状态设计来演示JS开发人员在为域建模时面临的常见困难,展示求和类型如何缓解这些困难,并回顾一些旨在将求和类型移植到语言中的库。


https://github.com/reactjs/redux

Redux.js是受Elm架构启发的“可预测状态容器”。 从复杂的Immutable.js Record到简单的POJO(普通的旧JavaScript对象),开发人员可以以他们希望的任何形式表示其应用程序的规范状态数据。 提取并显示可爱小猫列表的应用程序的状态可能很简单:

  const initialState = { 
小猫:[] //还没有小猫kitten
}

开发人员在签名为(oldState, action) -> newState的“ reducer”函数中指定基于Redux的应用程序的状态逻辑。

 功能约简(oldState,action){ 
如果(action.type ==='GOT_KITTENS'){
return {小猫:action.kittens} //替换小猫
}
return oldState //默认,什么都不做
}

给定一个表示应用程序事件的“操作”对象,化简器确定如何产生新状态。

  newState = reducer(initialState,{ 
类型:“ GOT_KITTENS”,
小猫:['Snuggles','Mittens']
})console.log(newState)// {小猫:['Snuggles','Mittens']}😺

用户界面代码(例如React组件)可以随后读取状态,从而创建小猫列表。

  const currentState = reduxStore.getState() 
const listItems = currentState.kittens.map(kitten =>
  • {kitten.name}

  • 另外:如果您以前从未使用过 JSX ,则上述内容可能会令人不安。 领域特定的语言可 编译为Vanilla JS:

      const currentState = reduxStore.getState() 
    const listItems = currentState.kittens.map(kitten =>
    React.createElement('li',null,小猫。名字)

    爱丽丝梦游仙境》,第一版。 牛津大学Bodleian图书馆。 刘易斯·卡洛尔(Lewis Carroll)是一位出色的数学家兼作家。

    作为勇敢的JS开发人员,我们接下来可能会尝试根据状态而不是length来区分状态。 如果我们使用null表示已卸载的小猫怎么办?

      const initialState = { 
    小猫:null
    }

    禁止,出现通配错误。

     错误:无法读取null的属性“长度” 

    从某种意义上说,这次我们很幸运-代码突然发出嘈杂的声音。 当然,问题在于null值不能具有属性,因此我们旧的UI代码检查kittens.length损坏了。 解决方法不是特别困难:

      const小猫= reduxStore.getState()。kittensif(!kittens){//正在加载小猫

    正在调用小猫!

    }否则if(!kittens.length){//已加载小猫但为空

    对不起,没有可用的小猫。

    }其他{//小猫已加载并且可以显示为返回(
      {
      pussys.map(小猫=>
    • {kitten.name}


    • }
    )}

    我们已经在上面注释了每种情况的含义,应该被认为是一种代码气味; 它表明我们的解决方案不是很语义化。 无论如何,单元测试都通过了,应用程序已部署,并且几天后一切似乎都很好。 直到…

    僵尸袭击时

    https://zh.wikipedia.org/wiki/One_Piece
     错误:无法读取null的属性“ join” 

    怎么办? 我们不是已经解决了吗? 嗯,但是此错误来自另一个组件:

      const小猫= reduxStore.getState()。kittensreturn 

    '已知的小猫包括:'+ kits.join('&')

    哎呀。 有人忘记了,或者从来没有听说过, kittens有时可能是null 。 监督一时未能引起注意,因为在拿到小猫之前,没有人运行过应用程序的这一部分。 这是一个陷阱,等待右边缘的情况出现。

    随着时间的流逝,会添加和/或发现多个类似的故障。 只要开发人员state.kittens语义state.kittens视为小猫的集合,他们就会继续尝试将其用作数组-即使有时不是一个数组。

    走出煎锅……

    http://gunshowcomic.com/648

    快速发展,随着产品需求成倍增加,各种临时解决方案也试图破坏我们的状态。 用户故事现在指定小猫需要有四只可见的 表示形式:卸载,加载,获取(带有数据)和失败(带有错误)。 在整个应用程序中,都在处理state.puppiesstate.bunnies类似需求。 状态树的某些叶子使用unloadedloading的字符串,而不是使用单个值null ,但这破坏了一些虚假的检查: if (!state.bunnies)现在是一个错误。 在HTTP失败的情况下, state.puppies叶子将被Error对象替换,从而在使用的代码中强制进行冗长if (state.puppies instanceof Error)可靠的检查if (state.puppies instanceof Error) 。 同时, state.kittens是具有.collection数组和.isError布尔值的对象,强制采用不同的处理方式- if (!state.kittens.isError) return state.kittens.collection[0] 。 也许有人认为某个值可能为false null ,而每个null都代表不同的东西……祝您好运。 由于要跟踪大量的临时性重制案例,因此开发人员经常会忘记处理其中一些案例,尤其是当表示形式前后矛盾且缺乏表现力时。

    https://www.tidydesign.com/blog/2012/09/free-paper-tag-image/

    我们几乎完成了对求和类型的定义,但是我们缺少一个关键特征,该特征将它们与(非常相似) 联合类型区分开来。 假设我们将名称部分的总和类型定义为名字或姓氏,其中每个都是字符串:

      // NamePart可以是名字字符串或姓氏字符串 
    总和类型NamePart =字符串| StringnamePart1 ='Wilson'//这是名字还是姓氏?
    namePart2 ='Ashley'//这是姓氏吗?

    当我们在野外遇到一个值时,我们知道它的类型是String的事实还不足以知道它是来自NamePart字符串的第一选择还是第二选择。

    为此,我们需要以某种方式用“ 标记标记值。感兴趣的值将不仅由字符串本身组成,而且还带有符号标识符,使开发人员可以清楚地知道哪个所属的组成类型:

      namePart1 =  
    namePart2 =

    嗯,现在我们确切地知道'Ashley'和“ Wilson'分别扮演什么角色。

    ReasonML是一种带有类型推断的不纯功能,急切求值的强类型语言。 本质上,它是OCaml的类似于JavaScript的语法,可以编译为本机代码,JavaScript,甚至可以编译回OCaml。 对于JS本机来说,这是一种开始以一种类型化,功能化的样式进行编码的好方法。 总和类型在ReasonML中称为变体

    Haskell是一种纯函数式,延迟评估的强类型语言,具有类型推断功能。 尽管Haskell也用于实践目的,但它在理论上有很高的关注度。 它非常强大,但异常简洁。

    https://jonsibal.deviantart.com/art/Superman-Secret-Origin-148465087

    现在,您可能不耐烦地想知道所有这些理论上的混乱是什么意思。 回想一下我们先前发现的两个问题:缺乏区分个案的语义方法,以及使用多态数据时的人为错误。

    总和类型肯定可以解决第一个问题。 通过添加构造函数标记,现在每个值都带有其自己的元数据标识符,该标识符解释了数据的大小写。

    我们如何解决第二个问题? 啊,这就是求和类型发光的地方: 模式匹配

    模式匹配是一种语言支持的语法,可以一次执行两件事: 识别值代表哪种情况,然后从该值中提取数据。 它使sum类型的用户可以轻松,声明性地并安全地使用该类型中的值。 它可以防止用户通过反转控制错误地处理值。 用户不再抢先尝试检查未知类型的属性(可能不存在),而是提供所有可能的类型处理案例以产生结果。

    让我们通过一个示例area函数(带有签名Shape -> Float查看shape上的图案匹配。

    ReasonML中的模式匹配

    ReasonML函数可以直接指定它采用的shape 。 我们如何使用shape ? 通过使用switch匹配所有标签:

    civiltale.origamitower.com

    Folktale是一个通用的功能库,除了adt/union以外,还带有其他工具。 这在上表中有点夸大其数字,但它同时也是标记联合的最精彩(IMHO)讨论之一,因此我们将对其进行介绍。 下面的嵌入式代码是可编辑的,请自己运行。

    https://github.com/fantasyland/daggy

    Daggy是功能性JS规范和工具的更广泛的Fantasy-Land生态系统的一部分。 该文档很少,只有两个难以解释的示例代码段; 但是,我们无法理解。

    编辑并运行我!
    • 最大的敲门声:开发人员仍然可以直接访问.radius 。 这可能会诱使他们尝试从未知形状中抓住.radius
    • 包括一个集成的产品类型是一个很好的选择。
    • 仅声明字段名称是极简主义,但是它甚至使我们无法执行类型检查。
    • 使用位置字段比强制解构功能更强大,因为您可以始终使用单个字段并根据需要对其进行解构。
    • 没有多余的检查,也没有失败的额外案例,也没有明显的计划来添加。
    • 没有后备案例,也没有明显的计划增加后备案例。

    Daggy允许通过惯用的原型继承进行扩展—向Shape.prototype添加方法将允许CircleRectangle委托给该方法。

    联合类型的形状

    方便地, shape是该库文档中演示的示例之一(此处略有改动)。

    编辑并运行我!
    • 最大的敲门声:开发人员仍然可以直接访问.radius 。 这可能会诱使他们尝试从未知形状中抓住.radius
    • 我们回到稍微尴尬的Point.Point因为没有明显的产品类型。
    • 我们获得了声明性字段名的所有好处以及验证,包括使用内置和预定义类型的声明性验证。 Union-type很好地进行了类型检查。
    • 在定义和构造类型的值时相对无缝地处理数组或对象,尽管在匹配过程中仅定位参数。
    • 尽管存在问题(Union-Type#52),但尚无详尽检查。
    • 在多余的情况下不会失败,也不会添加明显的计划。
    • 有一个后备情况_ ,尽管它本身对shape没有帮助。

    乍一看,联合类型是功能最全面的示例之一。 它包括原型继承,一些精美的工具,例如实例和咖喱静态case函数, caseOn ,递归类型,后备语法等变体。

    值得注意的是,发现的其他一个库(JAForbes / sum-type)是基于联合类型的,但具有与sanctuary-def项目相关的增强功能。

    所以……我们该使用哪个?

    这只是库的一小部分,上面的评论并非要对可行性,实现细节和其他问题做出最终判断。 相反,其目的是查看人们迄今为止如何尝试解决此问题,并考虑我们希望这样的图书馆如何工作。 例如,我们没有涉及潜在的重要功能,例如序列化。 简而言之,鉴于JavaScript的动态类型性质和各种现有的极端情况,任何尝试实现求和类型的库都可能会产生一些意见,有些问题尚待解决。 例如,所检查的三个库中没有一个阻止开发人员直接尝试从求和类型中获取属性,即使不是该类型的每个成员都具有该属性。

    在本文的其余部分中,我将使用并集类型,主要是因为它包括声明性类型检查和后备大小写语法。

    一个半月以前,本文以一个JavaScript用例示例:Redux状态树开头。 回想一下,我们正在努力对各种状态进行编码,例如“未加载”,“正在加载”,“已加载数据”和“因错误而失败”。很明显,我们可以将这些状态表示为求和类型。

    如果我们在状态树中还有其他叶子( state.bunniesstate.puppies等),它们也将是四种Leaf类型之一。 我们使用的代码将使用联合类型的case函数来确定要显示的UI。

    您很可能已经注意到Redux的动作对象的一些熟悉之处。 为什么……他们自己只是一个加标签的工会! 动作可以是一定数量的对象之一,每个对象都有一个type (标签),并且每个对象都可能具有许多其他属性(使其成为产品类型)。 在化器中,我们根据type #mindblown打开每种case 。 那么为什么不将动作表示为真实的和类型呢?

    不幸的是,Redux目前大胆地宣称动作对象现在是POJO,没有精美的库构造。 但是这个想法是合理的,实际上恰恰是最早启发Redux的原因。 Redux基于Elm,其中包括带标签的联合作为语言功能。 认识到动作是求和类型的成员,我们已经整整走了。

    Redux结论

    那么我们获得了什么? 我们不再使用原始类型对各种互斥的情况进行编码,从而彻底改变了方向盘。 并且,当我们要提取数据时,请注意处理所有可能的情况。 这不是完美的-类型检查是有限的,毫无疑问要考虑边缘情况,等等。但是,在表达能力,整洁性,一定程度的安全性以及更多方面都应具有吸引力。 像这样明确地为您的域建模是使用类型化语言的自然部分,并且凭借所有JavaScript所宣称的灵活性,采用其中的一些好处是有意义的。


    Redux状态是本文的一个激励示例,但总和类型通常如此有用,以至于不能不理会它们的“最大成功”。这是很可惜的。这些结构是如此基础,以至于它们经常被包含在其他语言的标准模块中。 以下示例将采用伪代码……看看是否可以用真实语言实现它们。

    也许/选项

    也许类型是一些数据,或者什么都不是。

     总和类型也许=什么| 只是一切firstOfList = list => 
    如果(!list.length)不返回
    否则返回Just(list [0])res =匹配firstOfList([])
    什么都没有=>“对不起,那里什么都没有。”
    只是(某物)=>'啊,发现了一些东西:'+ somethinglog(res)//'对不起,那里什么都没有。

    不再有-1undefinednull产生混乱。 现在,诸如findIndexfindfindById可以返回Maybe值; 然后,消费代码将进行模式匹配,以决定如何处理实际的NothingJust something Nothing视情况而定)。 尝试编写一个safeDivide函数,该函数在被零除的情况下将返回Nothing

    缺点清单

    这个经典的,基于递归,基于闭包的功能性链表是功能性编程语言的主力军。 基本上,列表可以是空列表Nil ,也可以由元素和以下列表构成: Cons x xs

     总和类型列表=无| 缺点什么ListmyList = Cons(1,Cons(2,Cons(3,Nil)))addList = someList => 
    匹配someList
    零=> 0
    Cons num xs => num + addList(xs)log(addList(myList))// 6

    二叉树

    总和类型擅长处理树结构,这就是为什么它们在编写编译器时非常方便的原因之一。

     总和类型Tree = Leaf | 任何树TreemyTree = Tree(1, 
    树(0,叶,叶),树(2,
    Tree(1.5,Leaf,Leaf),Tree(2.5,Leaf,Leaf)))addTree = someTree =>
    匹配someTree
    叶=> 0
    Tree num left right right => num + addTree(left)+ addTree(right)addTree(myTree)// 7

    当我开始为本文做笔记时,我告诉一些朋友,这将是“一篇快速的博客文章”。事实证明,我想说的总和类型比我最初意识到的要多得多。 如果您已经做到了,那么恭喜您! 我希望您喜欢它,并且有兴趣尝试在下一个JavaScript项目中使用求和类型。 语言的性质可能会使尝试不完美,但是不完美的改进仍然是一种改进。 随着将来可能包含本机模式匹配,香草JS中的求和类型可能变得更加可行。

    资源资源

    这是我发现有助于研究该主题的部分资源列表,可以推荐进一步阅读。

    语言文件

    • ReasonML:变体! [原文如此]。 另请参见ReasonMLHub:变体
    • Haskell:代数数据类型
    • 榆木:工会类型
    • F#:歧视工会
    • 锈:枚举

    JS库文档

    • 民间故事:联盟(尤其是很好的文档文章混合体)
    • 联合型
    • 流量:工会

    文章和文章系列

    • Waleed Khan,《联盟与求和类型》。 优秀的文章,确实帮助我理解了这些概念之间的区别。
    • 加布里埃尔·冈萨雷斯(Gabriel Gonzalez),求和类型。 Haskell中的示例。
    • Scott Wlaschin,《娱乐与盈利的F#:使用类型进行设计》。 进一步探讨类型如何支撑应用程序的整个逻辑。 还可以查看他在同一主题上的演讲。
    • Joel Burget,代数数据类型的代数(和微积分!)。 深入探讨ADT的理论和数学方面。

    维基百科

    • 标记联盟
    • 代数数据类型

    至理名言。