【问题标题】:Why are primitives not needed for haskell data constructors?为什么 haskell 数据构造函数不需要原语?
【发布时间】:2021-06-22 17:18:38
【问题描述】:

我是 Haskell 编程和学习类型系统的新手,我无法掌握空数据构造函数的基础。..

以以下为例:

data Color = Red | Green | Blue | Indigo | Violet deriving Show

genColor:: Color
genColor = Red 

据我了解,Red、Green、Blue.. 是无效数据构造函数,在使用时会构造“颜色”。

我难以理解的是,在传统的 OOP 语言中,您必须指定类型底层的原语——例如。颜色是否为字符串、int、float 等。

在 Haskell 中,上面的代码运行良好,为什么不需要呢?像这样构建类型系统的基本原理是什么?谢谢,所有帮助将不胜感激:)

【问题讨论】:

  • 几乎每种语言都提供这样的枚举——例如,Java 和 C++ 中的 enum、Scala 中的 Enumeration 或 Python 中的 Enum
  • 数据构造函数原语。基本上,该语言允许您为新类型定义文字。 Red 不仅仅是一个“真实”值为0"Red" 的变量; Red值。
  • @DanielWagner IIRC, Enums 在 Python 中有点不满意,因为它们确实需要一些其他值来识别它们,它们不能只是“做自己”docs.python.org/3.9/library/enum.html
  • @DanielWagner C++ 的枚举也有一个基础类型(旧的无范围枚举和新的枚举类)。我认为可以公平地说 Java 的枚举确实没有底层类型,尽管它们确实公开了一个您可以获取的标签。

标签: haskell


【解决方案1】:

我想起了 Alexis King 的一篇出色的1 博文:Types as Axioms。它将您假设的减法视角(“我定义一个枚举类型以限制变量可能采用的整数值集,并为清楚起见给这些整数命名”)与加法视角进行对比代数数据类型实际上是关于(“既然我定义了一个全新的类型,我还必须定义 全新的值 来容纳它,它与任何其他类型的值没有特别的关系” )。


1 多余,因为她所有的博文都很棒

【讨论】:

  • 好文章!感谢分享:)
【解决方案2】:

Haskell 只是让您无需编写样板文件

# Python
class Color(Enum):
    Red = "Red"
    Green = "Green"
    Blue = "Blue"
    Indigo = "Indigo"
    Violet = "Violet"

相反,Red 值;它们实际上是Color 类型的用户定义文字。 (Haskell 本身提供的抽象中不存在与原始 machine 类型值的任何其他关联。)

任何其他“基础”值都不是类型本身的属性,而是类型类实例提供的投影,例如 Enum

-- e.g. fromEnum Red == 0
-- toEnum is necessarily partial, since
-- Color has a strictly smaller cardinality than Int.
-- fromEnum is not surjective for the same reason.
instance Enum Color where
    fromEnum Red = 0
    fromEnum Green = 1
    fromEnum Blue = 2
    fromEnum Indigo = 3
    fromEnum Violet = 4
    toEnum 0 = Red
    toEnum 1 = Green
    toEnum 2 = Blue
    toEnum 3 = Indigo
    toEnum 4 = Violet

Show:

-- e.g. show Red == "Red"
instance Show Color where
    show Red = "Red"
    show Green = "Green"
    show Blue = "Blue"
    show Indigo = "Indigo"
    show Violet = "Violet"

这种“显而易见”的实例当然可以由编译器自动派生,但如果您真的不在乎 Color 值是否具有关联的 StringInt 值,则可以完全省略。

【讨论】:

  • 您还可以使用dataToTag# 访问基础值(仅适用于 GHC)。
  • @Noughtmare 我认为这不是 Haskell 的一部分。它不仅需要 GHC,而且仅考虑到 GHC 的 Haskell 实现才有意义。
  • 我认为几乎所有的 Haskell 实现都使用标签来表示“nullary”数据类型,所以dataToTag# 对其他编译器来说确实有意义(尽管可能返回一个普通的Int 而不是未装箱的Int#) .
  • @Noughtmare,我似乎记得 John Hughes 谈到了一个非常早期的实现,但由于现代微处理器设计,这种方法似乎不太可能再次出现。
【解决方案3】:

许多语言都有原始的复合类型,例如数组,或具有命名字段的结构,或元组,或者你有什么。

Haskell 中主要的原始复合类型是代数数据类型或 ADT。它是“产品总和”或“结构的标记联合”。 OOP 语言通常具有明显的原始产品(结构)类型。在某些语言中,比如 C++,实际上有一个 struct 原语,但在大多数 OOP 语言中,类本身就是明显的产品类型——你有一堆由类定义的字段,一个对象是一个值每个字段的值的乘积。

OOP 语言很少有明显的原始求和(标记联合)类型。但是,有一个隐含的。每次调用对象上的方法并依赖基于对象类的分派时,您实际上是在使用 sum(标记联合)类型。您有一个变量,其类型是接口或抽象超类(取决于语言),变量的值是一个对象,其类实现了接口或继承自超类。当您在变量上调用方法时,会咨询该类以分派到正确的方法。等效地,您有一个变量,其类型为总和(标记联合);变量的值对应于总和的一个组成部分(标记联合的成员);并且当您在变量上调用方法时,会参考标记以确定您实际拥有总和的哪个分量(标记联合的成员),因此可以调用正确的方法实现。

这个类比并不像看起来那么疯狂。在您的脑海中想象一个在叶子中具有整数值的二叉树的 OOP 实现。您可能正在想象NodeLeaf 类实现一个通用的Tree 接口,例如提供遍历方法。现在,考虑一个 Haskell 实现:

data Tree = Leaf Int | Node Tree Tree
            ^^^^-------^^^^- the classes
     ^^^^- the interface


traverse :: Tree -> [Int]
^^^^^^^^- the method
traverse (Leaf x) = [x]
traverse (Node l r) = traverse l ++ traverse r

在实践中,许多 OOP 设计自然地以这种方式映射到 Haskell,接口、类和方法映射到数据类型、构造函数和普通的旧函数。

这与您的问题有什么关系?好吧,如果你有这样的数据类型:

data Color = Red | Green | Blue | Indigo | Violet

您不应该将Color 视为其他一些原始类型(例如整数)的别名,并将其值RedViolet 映射到值1 到5。这就像考虑OOP 接口 Tree 作为某些原始类型的别名(什么,双打?字符串?),别名“值”NodeLeaf 对应于原始值,例如 2.0"hello"

相反,将Color 视为原始复合类型——产品的总和或结构的标记联合,其中产品/结构恰好是空的(零字段)。结果,剩下的就是标记总和的标签(如Red 等)。表示不需要“其他”原始类型,因为复合乘积和原始类型底层原始类型。

【讨论】:

  • “映射到...多态函数的方法”——为什么是多态的?
猜你喜欢
  • 1970-01-01
  • 2011-02-25
  • 1970-01-01
  • 1970-01-01
  • 2020-03-10
  • 2012-01-17
相关资源
最近更新 更多