在 Haskell 中添加 (:) 函数定义

Prepend (:) function definition in Haskell

我想更好地理解我正在学习的课程中的一些代码。我使用 Ghci 推断 (:) 的类型为 (:) :: a -> [a] -> [a]。我想知道这个功能到底是怎么实现的,但是我找不到任何东西。

第一级答案是它只是列表的数据构造器。如果语法有效,您可以想象:

data [a] = [] | a : [a]

比较 Complex:

的定义
data Complex a = a :+ a

Two:

data Two a = Two a a

如果需要,您可以定义自己的 list-alikes;说

data List a = Nil | Cons a (List a)

然后 (:)Cons 将具有基本相同的实现。

但显而易见的 follow-up 问题是:数据构造函数是如何实现的?回答这个问题会让我们脱离 Haskell-the-language 的领域,进入特定 Haskell 实现的领域。尽管有一点错误,但对 GHC 的实现非常有用的一个心智模型是:当你将数据构造函数应用于足够多的参数时,你会得到一个指向内存块的新指针,该内存块有一个数字(告诉它是许多构造函数)后跟一些更多的指针(构造函数的每个字段一个)。所以,对于列表,你会有这样的东西:

term   | memory
-------+-----------------------------------------------------
       | ------------
[]     | | 00000000 |
       | ------------
       |     tag
       |
       | ----------------------------------------------------
a : as | | 00000001 | 00101110100111000 | 00001010111011000 |
       | ----------------------------------------------------
       |     tag      pointer to a        pointer to as

:是列表类型的数据构造函数之一,所以它不是像++那样用普通Haskell代码作为实现的函数。列表的特殊语法有点妨碍解释,所以我将从不同的类型开始,然后再回到真正的列表。让我们看看这个:

data List a
  = Nil
  | Cons a (List a)

这定义了两个构造函数:NilConsNil 没有任何参数,所以它只是 类型 List a 的值(对于任何 a)。 Cons 有两个参数;它需要一个 a 和一个 List a 来产生一个 List a.

类型的值

这意味着我们可以像普通函数一样使用Cons。我们传递给它参数并得到一个 return 值。这让我们想知道该功能的实现是什么。普通函数由code定义;当您应用一个函数时,您 运行 它的代码 1,并且该代码产生的任何内容都是该函数的 return 值。当您应用数据构造函数时,运行 是什么代码?

答案其实是none。数据构造函数是特殊的,尽管我们可以像应用函数一样应用它们。数据构造函数是我们将 new 种值引入程序的方式,因此无法使用代码定义它们(这将导致 existing的东西,不是新的种值)。

数据构造器也是模式匹配的基本构建块。您可以检查一个值以查看它是否是应用于其他两个值的 Cons 构造函数,如果是,则访问它们。当你有一个由代码而不是数据构造函数产生的值时,没有办法判断它是否是特定函数的产物(更不可能的是“un-apply”代码并弄清楚是什么论点是)。但是数据构造器可以这样 un-applied,这正是模式匹配起作用的原因。

Haskell提供数据构造函数的这两个特性(创建新类型的值,并能够un-apply它们来获取参数值)的方式,并不是通过运行宁码。相反,当我们说 Cons True Nil 时,参数只是存储 as-is。 Cons True Nil 没有 运行 任何代码来生成某些东西; Cons 只是一个包含两个槽的值,而 Cons True Nil 只是一个 Cons,这些槽填充了 TrueNil。这保证了这确实是一种新的值,任何现有类型都无法实现(因为它们都有自己的数据构造函数,而不是新的 Cons)。它可以很容易地分辨出 Cons 的参数是什么:Haskell 可以只查看两个插槽。2

我答应过我会把它带回真正的 Haskell 列表,让我们开始吧。 [a] 类型及其构造函数是 built-in 到 Haskell,但它们的行为与我上面定义的 home-made 列表类型完全相同!由于使用了不寻常的语法,有时很难看出它们有多“正常”,但看看这些:

-- definition 1
data List a
  = Nil
  | Cons a (List a)

-- definition 2
data List a
  = Nil
  | a `Cons` List a

-- definition 3
data [] a
  = []
  | (:) a ([] a)

-- definition 4
data [a]
  = []
  | a : [a]

所有这些都定义了一个具有相同结构的类型(与内置列表类型的结构相同)。

定义 1 和 2 实际上定义相同的类型;您可以在带有两个参数的名称周围使用反引号 (`) 以在其参数之间而不是之前应用它的中缀。这适用于构造函数名称,您可以在构造函数的定义中做到这一点。

定义 3 和 4 是类似的模式,但使用内置列表类型使用的实际“名称”。您实际上不能在 Haskell 代码中输入其中任何一个,因为在您自己的定义中使用特殊列表语法是无效的,但是内置列表类型的行为是 as-if 它是由其中之一定义的。在定义 3 中,我使用普通前缀应用程序 [] 类型构造函数 3[] a 就像我在其他两个定义中使用 List a 一样) ,并类似地使用前缀符号应用 : 构造函数(用括号将运算符 : 括起来,就像我们可以对 (+) 1 2 等任何运算符所做的那样)。虽然定义 4 反映了定义 2 的模式,使用中缀构造函数 :,并且还完全使用列表类型的独特 [a] 语法。

所以你看,:实际上没有实现(在Haskell级别)。 True : [] 只是 应用于参数 True[]: 构造函数;它不做任何工作来评估其他任何东西。它 作为“前置运算符”也很方便,但是列表类型的定义方式使 : 变得特别。例如,++ 运算符(用于附加)需要根据 前置定义 因为我们 定义了 列表结构前置(即 :)。 Prepending 不需要根据其他任何东西来定义,因为它是 wht 我们根据以下定义列表。这就是为什么你找不到 : 的任何定义的原因(好吧,事实上标准列表类型内置于编译器中,所以你甚至找不到它的 data 声明).


1 参数变量绑定到您提供的参数值。


2这些“仅存储值”和“查看插槽”的能力只是 built-in Haskell 的基本概念.它们是定义语言 Haskell 的基石,而不是用普通 Haskell 编码的东西。但是他们当然 编译器提供的较低级别代码的实现;如果您有兴趣,Daniel Wagner 的回答对此有一些讨论。


3 这是很难看出列表类型有多“正常”的原因之一,对于如何 write 有特殊规则 只列出 none 他们 的行为方式 。在值级别上,[] 符号是一个零参数的数据构造函数,用方括号包围的东西 [True] 是构造列表的特殊语法糖(涉及 both 构造函数,在我们的例子中:True : []).

而在类型级别上,相同的符号 [] 是一个具有一个参数的类型构造函数,并且将某些内容包裹在方括号中,例如 [Bool] 是应用 [=36= 的特殊语法糖] 为它键入构造函数(在我们的例子中:[] Bool)。

如果我事后从头开始重新定义 Haskell 语言,我会将列表类型构造函数命名为 List,保留方括号语法仅用于在术语中定义列表值等级。我什至可能会为空列表构造函数使用“正常”名称而不是 [],留下方括号纯粹涉及特殊语法糖,根本不涉及基础类型的实际定义,这在拼写和行为上都是完全普通的。列表类型的语法很可爱,而且非常短,但我认为它最终会阻碍理解而不是促进它。 (对于初学者;一旦你充分习惯了这两种方式都可以)

虽然 [] 作为空列表构造函数的符号非常令人满意,因为方括号语法糖仍然会将 [] 定义为无论如何编写空列表的一种方式,所以也许我很想保留那个。