在 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)
这定义了两个构造函数:Nil
和 Cons
。 Nil
没有任何参数,所以它只是 是 类型 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
,这些槽填充了 True
和 Nil
。这保证了这确实是一种新的值,任何现有类型都无法实现(因为它们都有自己的数据构造函数,而不是新的 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
,保留方括号语法仅用于在术语中定义列表值等级。我什至可能会为空列表构造函数使用“正常”名称而不是 []
,留下方括号纯粹涉及特殊语法糖,根本不涉及基础类型的实际定义,这在拼写和行为上都是完全普通的。列表类型的语法很可爱,而且非常短,但我认为它最终会阻碍理解而不是促进它。 (对于初学者;一旦你充分习惯了这两种方式都可以)
虽然 []
作为空列表构造函数的符号非常令人满意,因为方括号语法糖仍然会将 []
定义为无论如何编写空列表的一种方式,所以也许我很想保留那个。
我想更好地理解我正在学习的课程中的一些代码。我使用 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)
这定义了两个构造函数:Nil
和 Cons
。 Nil
没有任何参数,所以它只是 是 类型 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
,这些槽填充了 True
和 Nil
。这保证了这确实是一种新的值,任何现有类型都无法实现(因为它们都有自己的数据构造函数,而不是新的 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
,保留方括号语法仅用于在术语中定义列表值等级。我什至可能会为空列表构造函数使用“正常”名称而不是 []
,留下方括号纯粹涉及特殊语法糖,根本不涉及基础类型的实际定义,这在拼写和行为上都是完全普通的。列表类型的语法很可爱,而且非常短,但我认为它最终会阻碍理解而不是促进它。 (对于初学者;一旦你充分习惯了这两种方式都可以)
虽然 []
作为空列表构造函数的符号非常令人满意,因为方括号语法糖仍然会将 []
定义为无论如何编写空列表的一种方式,所以也许我很想保留那个。