【问题标题】:Why do we need "Algebraic Data Types"?为什么我们需要“代数数据类型”?
【发布时间】:2015-10-23 22:24:21
【问题描述】:

我读过一些关于代数数据类型的解释:

这些文章给出了非常详细的描述和代码示例。

起初我以为代数数据类型只是为了轻松定义一些类型,我们可以用模式匹配来匹配它们。但是看了这些文章,我发现里面根本没有提到“模式匹配”,内容看起来很有趣,但比我想象的要复杂得多。

所以我有一些问题(这些文章中没有回答):

  • 为什么我们需要它,比如说,在 Haskell 或 Scala 中?
  • 有了它我们能做什么,没有它我们不能做什么?

【问题讨论】:

  • 您希望元组返回多个结果,并且您希望 sum-types 做出明确的选择。 (当然这只是沧海一粟——你可以写关于这些东西的书)。您提供的链接可能具有误导性 - 您可能会认为您只需要这些东西来处理 ivory-tower-arkane-magic 的东西,但它实际上只是一种以理智的方式管理数据的方法(特别是如果您没有类、继承和变异引用供您使用)
  • @Carsten 我觉得你理解我的困惑。感谢您的评论,并希望您能说出更多信息:)
  • 只是为了表示警告,首字母缩略词“ADT”也用于表示“抽象数据类型”,这是与“代数数据类型”相反的概念。因此,应谨慎使用首字母缩略词“ADT”,即根本不用。
  • @pigworker 谢谢,已修复
  • 如果你想知道人们为什么需要数字,不要从数论课程开始。它不会提及诸如平衡您的支票簿或数羊之类的事情。我夸大了一点。

标签: scala haskell algebraic-data-types


【解决方案1】:

我们应该从 Haskell wiki 文章 Algebraic Data Types 开始

这里,很快,只是我的愿景:

  • 我们需要它们以旧的面向对象方式(或实际上以旧的基于类别的方式)对业务逻辑进行建模,并使其更加类型安全,因为编译器可以检查您是否匹配了所有可能的选择。或者,换句话说,您的函数是total,而不是部分函数。最终,它使编译器能够证明代码的正确性(这就是推荐使用密封特征的原因)。因此,您拥有的不同类型越多越好 - 顺便说一句,泛型编程可以帮助您生成更多类型。
  • 标准特性:我们可以将类型表示为对象的“集合”,我们可以将对象与类型匹配(使用模式匹配),甚至可以使用匹配器解构(分析)它。我们还可以使用类型类动态地将行为(在编译时)添加到这种类型。常规类也可以,但在这里它使我们能够将代数模型(类型)与行为(函数)分开
  • 我们可以将类型构造为不同对象/类型的products/coproducts。您可以将代数类型系统视为一个集合(或更一般地 - 作为一个笛卡尔封闭类别)。 type Boolean = True | False 表示布尔值是 TrueFalse 的并集(联积)。 Cat(height: Height, weight: Weight)HeightWeight 的“元组”(更一般地 - 产品)。 Product (more-less) 表示来自 OOP 的“部分”关系,coproduct - “is”(但以相反的方式)。

它还为我们提供了一种以多方法样式在运行时调度行为的方法(就像在 CLOS 中一样):

  sealed trait Animal
  case class Cat extends Animal
  case class Dog extends Animal

 def move(c: Animal) = c match {
   case Cat(...) => ...
   case Dog(...) =>
   case a: Animal => ...//if you need default implementation
 }

类似 Haskell:

 data Animal = Dog | Cat //coproduct

 let move Dog d = ...
 let move Cat c = ...

代替:

trait Animal {
  ...
  def move = ...
}

class Cat(val ...) extends Animal {
  override def move = ...
}

class Dog(val ...) extends Animal {
  override def move = ...
}

附:从理论上讲,如果您以代数方式对世界进行建模并且您的函数是完全且纯的 - 您可以证明您的应用程序是正确的。如果它编译 - 它可以工作:)。

附注 2。我还应该提到,Scala 的类型层次结构中包含Any,它对类型安全性不太好(但对与 Java 和 IO 的互操作有好处),因为它破坏了 GADT 定义的良好结构。不仅如此,case class 还可能同时是 GADT(代数)和 ADT(抽象),这也减少了保证。

【讨论】:

  • 谢谢,这正是我在阅读这些文章之前所想的 ADT。但似乎他们在谈论不同的东西
  • 我相信,它实际上几乎和你想的一样,只是你可以(更多地-更少地)将它们视为一个集合data Boolean = True | False 意味着布尔值是真假的联合(副产品)。猫(身高:身高,体重:体重)是身高和体重的乘积
  • @Freewind 我添加了一些与范畴论的类比
【解决方案2】:

您提到的博客文章更多地是关于代数数据类型的数学方面,而不是关于它们在编程中的实际用途。我认为大多数函数式程序员首先是通过在某种编程语言中使用它们来了解代数数据类型,然后才研究它们的代数性质。

确实,这篇博文的意图从一开始就很明确:

在这一系列文章中,我将解释为什么 Haskell 的数据类型是 称为代数 - 不提范畴论或高级 数学。

无论如何,代数类型的实用性最好通过玩弄它们来获得。

假设,例如,您想编写一个函数来使平面上的两个线段相交。

def intersect(s1: Segment, s2: Segment): ???

结果应该是什么?写起来很诱人

def intersect(s1: Segment, s2: Segment): Point

但是如果没有交集呢?人们可能会尝试通过返回null 或抛出NoIntersection 异常来修补该极端情况。但是,当两条线段位于同一直线上时,它们也可能在多个点上重叠。在这种情况下我们应该怎么做?抛出另一个异常?

代数类型方法是设计一个涵盖所有情况的类型:

sealed trait Intersection
case object NoIntersection extends Intersection
case class SinglePoint(p: Point) extends Intersection
case class SegmentPortion(s: Segment) extends Intersection

def intersect(s1: Segment, s2: Segment): Intersection

在许多实际案例中,这种方法感觉很自然。在其他一些缺乏代数类型的语言中,人们不得不求助于异常、nulls(另见the billion-dollar mistake)、非密封类(使得编译器无法检查模式匹配的详尽性),或者到语言提供的其他功能。可以说,OOP 中的“最佳”选项是使用Visitor pattern 在没有此类功能的语言中编码代数类型和模式匹配。尽管如此,在 scala 中直接支持该语言会更方便。

【讨论】:

    猜你喜欢
    • 2013-10-18
    • 2020-12-17
    • 1970-01-01
    • 1970-01-01
    • 2015-06-05
    • 1970-01-01
    • 2013-05-28
    • 1970-01-01
    相关资源
    最近更新 更多