请确认或更正我对这个Haskell代码片段的"英文解释"

14

我是一名C#开发人员,正在阅读"Real World Haskell"这本书,以便真正理解函数式编程。这样当我学习F#时,我就可以真正领会它,而不仅仅是“用C#代码写F#代码”。

今天,我遇到了一个例子,我认为我已经理解了3次,然后才发现漏掉了某些东西,更新了我的解释,并递归(也诅咒过,相信我)。

现在我相信我确实理解了它,并在下面写了一个详细的“英文解释”。Haskell专家们可以确认我的理解,或指出我所错过的内容吗?

注意:Haskell代码片段(直接引用自书中)定义了一个自定义类型,该类型旨在与内置的Haskell列表类型同构。

Haskell代码片段

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

编辑:经过一些回应,我发现了一个误解,但是我不太清楚Haskell的“解析”规则会纠正这个错误。所以我包括了我的原始(不正确的)解释,然后是一个更正,最后是仍然不清楚的问题。

编辑:这是我原来(不正确的)的代码片段“英语解释”

  1. 我正在定义一个类型称为“List”。
  2. List类型是参数化的。它有一个类型参数。
  3. 有两个值构造函数可以用于创建List实例。其中一个值构造函数称为“Nil”,另一个值构造函数称为“Cons”。
  4. 如果使用“Nil”值构造函数,则没有字段。
  5. “Cons”值构造函数具有单个类型参数。
  6. 如果使用“Cons”值构造函数,则必须提供2个字段。第一个必需字段是List的实例。第二个必需字段是a的实例。
  7. (我有意省略了有关“定义Show”的任何内容,因为它不是我现在想重点关注的部分)。

更正的解释如下(改动用粗体标出)

  1. 我正在定义一个类型称为“List”。
  2. List类型是参数化的。它有一个类型参数。
  3. 有两个值构造函数可以用于创建List实例。其中一个值构造函数称为“Nil”,另一个值构造函数称为“Cons”。
  4. 如果使用“Nil”值构造函数,则没有字段。

    5.(此行已被删除…不准确)“Cons”值构造函数具有单个类型参数。

  5. 如果使用“Cons”值构造函数,则必须提供2个字段。第一个必需字段是a的实例。第二个必需字段是“list-of-a”的实例。

  6. (我有意省略了有关“定义Show”的任何内容,因为它不是我现在想重点关注的部分)。

仍然不清楚的问题

最初的困惑是关于代码片段中读取“Cons a(List a)”部分的内容。事实上,这是仍然不清楚的部分。

人们指出,“Cons”标记后每个项目都是一个类型,而不是值。因此,这一行表示“Cons值构造函数有2个字段:类型为'a'和类型为'list-of-a'的另一个字段。”

那非常有帮助。但是仍然有些不清楚。当我使用Cons值构造函数创建实例时,这些实例将第一个'a'“解释”为意思是“在此处放置传递的值”。但是他们不会以相同的方式解释第二个'a'。

例如,请考虑此GHCI会话:

*Main> Cons 0 Nil
Cons 0 Nil
*Main> Cons 1 it
Cons 1 (Cons 0 Nil)
*Main> 
当我键入“Cons 0 Nil”时,它使用“Cons”值构造函数创建List的一个实例。从0中,它了解到类型参数是“Integer”。到目前为止,没有混淆。
然而,它还确定第一个字段的值为0。但它对第二个字段的值什么也不确定...它只确定第二个字段的类型为“List Integer”。
因此,我的问题是,为什么第一个字段中的“a”意味着“这个字段的类型是'a',值是'a'”,而第二个字段中的“a”仅意味着“这个字段的类型是 'List of a'”?
编辑:我相信现在我已经看到了光明,多亏了几个回复。让我在这里表述一下。(如果某种方式仍然有错误,请告诉我!)
在片段“Cons a(List a)”中,我们正在说“Cons”值构造函数具有两个字段,第一个字段的类型为'a',第二个字段的类型为“List of a”。
这就是我们说的全部内容!特别是,我们什么也没说关于值!这是我缺少的一个关键点。
后来,我们想要创建一个实例,使用“Cons”值构造函数。我们将其键入解释器:“Cons 0 Nil”。这明确地告诉Cons值构造函数使用0作为第一个字段的值,并使用Nil作为第二个字段的值。
就是这样。一旦你知道值构造函数定义仅指定类型,一切就会变得清楚。
感谢大家的帮助回复。正如我所说,如果还有问题,请告诉我。谢谢。

1
我认为你的最终编辑完美地概括了它,它是完全正确的。 - Tom Lokhorst
1
这是一个非常好的问题风格,我认为我们需要更多这样的问题。 - Benjol
8个回答

8
“Cons”值构造函数有一个类型参数。

不对:当您声明“data List a”时,已经对其进行了参数化。这种有效的属性是,如果我有一个Nil :: List Int,我无法将其与Nil :: List Char交换。

如果使用“Cons”值构造函数,则必须提供2个字段。第一个必需字段是List的实例。第二个必需字段是a的实例。

您搞错了:第一个必需字段是a的实例,第二个字段是List的实例。

Real World Haskell的本章可能会引起您的兴趣。

不。一旦我们在类型中声明参数,我们就可以重用它,否则要说“应该在那里使用该类型”。这有点像a -> b -> a类型签名:a正在参数化类型,但然后我必须使用相同的a作为返回值。

不,这也不正确。

以下是一个有教育意义的示例,您可能已经看过其语法:

foo :: Num a => a -> a

这是一个函数的标准签名,它接受一个数字并对其执行某些操作,并给您另一个数字。然而,在Haskell中,“数字”的实际含义是实现“Num”类的某些任意类型“a”。

因此,这解析为英语:

让a表示实现Num类型类的类型,然后该方法的签名是具有类型a的一个参数,并且返回类型为a的值

数据也会发生类似的情况。

我感觉你可能对Cons规范中List实例也感到困惑:在解析时要非常小心。虽然Cons指定了一个构造函数,它基本上是Haskell将数据包装成的模式,但(List a)看起来像一个构造函数,实际上只是一种类型,就像Int或Double一样。a是一种类型,绝不是任何意义上的值。
编辑:针对最近的编辑做出回应。
我认为首先需要进行分解。然后我会逐点回答你的问题。
Haskell数据构造器有点奇怪,因为你定义了构造器签名,而且你不必做任何其他的脚手架。Haskell中的数据类型没有任何成员变量的概念。(注意:有一种更易于思考的备用语法,但现在让我们忽略它。)
另一件事是,Haskell代码很密集;它的类型签名也是如此。因此,预计会在不同的上下文中重复使用相同的符号。类型推断在这里也扮演着重要角色。
所以,回到你的类型:
data List a = Cons a (List a) | Nil
我把它分成几个部分:
data List a
这定义了类型的名称,以及稍后它将拥有的任何参数化类型。请注意,您只会在其他类型签名中看到这个。
Cons a (List a) | Nil
这是数据构造器的名称。这不是类型。但我们可以通过模式匹配来使用它,例如:
foo :: List a -> Bool foo Nil = True 注意,在签名中,List a是类型,而Nil既是数据构造器,也是我们模式匹配的“东西”。
Cons a (List a)
这些是我们插入构造器的值的类型。Cons有两个条目,一个是类型a,另一个是类型List a。
所以我的问题是,为什么第一个字段中的“a”表示“该字段的类型是'a',值是'a'”,而第二个字段中的“a”仅表示“该字段的类型是'List of a'”?
简单:不要把它看作是我们指定类型;把它看作是Haskell从中推断出类型。因此,对于我们的目的,我们只需在其中放置一个0,并在第二部分中放置一个Nil。然后,Haskell查看我们的代码并思考:
  • 嗯,我想知道Cons 0 Nil的类型是什么
  • 好的,Cons是List a的数据构造函数。我想知道List a的类型是什么
  • 好的,a用在第一个参数中,所以由于第一个参数是Int(另一种简化;0实际上是一种奇怪的东西,被分类为Num),所以这意味着a是Num
  • 嘿,那也意味着Nil的类型是List Int,尽管没有任何东西会实际上表明它

(注意,这并不是实际实现方式。Haskell在推断类型时可以做很多奇怪的事情,这也部分原因导致错误消息很糟糕。)


谢谢。这是我现在正在学习的章节。所以...当代码说“Cons a(List a)”时,我认为“Cons a”部分是在说明Cons值构造函数是有参数的。他们还没有涵盖参数化类型的语法,所以我猜想语法必须要重新声明“a”,如果你打算使用它。但是你的意思是不需要这样做吗?因此,那个“a”的意思不是那样的? - Charlie Flowers
好的,但这很令人困惑。第一个“a”似乎意味着“第一个字段是'a'的实例”,同时也意味着“第一个字段的值与他们传递给'a'的值相同”。换句话说,它指定了类型和值。但是第二个“a”只指定了类型。它说,“第二个字段必须是列表类型的'a'”。并且没有提到任何关于值的信息。是这样吗?如果是,什么规则决定了“a”何时表示值和类型,何时表示仅类型? - Charlie Flowers
请看我的编辑。当我们在声明“数据”时,我们正在指定一种模式,当我们稍后调用数据构造函数时,数据将“插入”该模式。这个模式的一部分是内部位的类型。 - Edward Z. Yang
好的,最后还有一点疑惑。似乎a被视为第一个字段的值。请看我的编辑过的问题。谢谢。 - Charlie Flowers
看我的回答。类型推断就是答案! - Edward Z. Yang
1
我现在明白了,很大程度上要归功于这里许多答案的结合。 "a" 和 "List a" 仅仅是类型。然后当我输入 "Cons 0 Nil" 时,我提供了两个明确的值。这两个值被分配给了两个字段,就这样。我理解了类型推断如何从第一个字段“传递”到第二个字段,因为 C# 泛型也是这样做的(尽管我听说 Haskell 在这方面更好)。谢谢。 - Charlie Flowers

4

比喻通常在各种方面都有所欠缺,但由于您了解C#,我认为这可能会有所帮助。

以下是我描述C#中List a定义的方式,也许可以澄清一些事情(或更有可能使您更加困惑)。

class List<A>
{
}

class Nil<A> : List<A>
{
    public Nil() {}
}

class Cons<A> : List<A>
{
    public A Head;
    public List<A> Tail;

    public Cons(A head, List<A> tail)
    {
        this.Head = head;
        this.Tail = tail;
    }
}

如你所见:

  • List 类型只有一个类型参数(<A>)。
  • Nil 构造函数没有任何参数。
  • Cons 构造函数有两个参数,一个类型为 A 的值 head 和一个类型为 List<A> 的值 tail

现在,在 Haskell 中,NilCons 只是 List a 数据类型的构造函数,在 C# 中它们也是独立的类型,所以这就是比喻失败的地方。

但我希望这能让你对不同的 A 代表什么有一些直观的感觉。

(请评论一下这个可怕的比较对 Haskell 的数据类型没有公正评价。)


1
是的,这很有帮助。它强调了“a”和“List a”只是类型的事实。当我键入“Cons 0 Nil”时,我稍后提供显式值。第一个字段变为“0”,因为我告诉它使用该值,而不是因为“a”在值构造函数的定义中使用。您的回复以及其他几个回复帮助我理解了这一点。 - Charlie Flowers
@CharlieFlowers 这不是 值构造函数 的定义,而是 List a 数据类型 的定义,其中包括其(这里有两个)数据构造函数的规范。这些构造函数由Haskell自动创建/定义,本质上类似于非常简单的结构体构造函数。 - Will Ness

3
Cons a (List a)

Cons的第一个字段是类型为"a"的值。第二个字段是类型为"List a"的值,即使用与当前列表参数相同类型的参数进行参数化的列表。

感谢您的回复。我现在知道您所说的是正确的,但我对为什么会这样有疑问。请查看我对另一个回复(来自Ambush Commander)的评论。基本上,第一个“a”指定类型和值,而第二个“a”仅指定类型。为什么会这样呢? - Charlie Flowers
不是的,第一个 a 指定了 Cons 构造函数的第一个参数类型为 a,第二个参数类型为 List a - Hynek -Pichi- Vychodil
好的,最后还有一点混淆。似乎a被视为第一个字段的值。请参见我编辑过的问题。谢谢。 - Charlie Flowers

3

是的,数据语法有点令人困惑,因为它混淆了名称和类型,并没有在语法上对它们进行明确区分。特别是在构造函数定义中,例如:

Cons a (List a)

第一个单词是构造函数的名称;其他每个单词都是一些预定义类型的名称。因此,aList a都已在作用域内(a通过"data List a"中的a引入了作用域),您正在说明这些是参数的类型。使用记录语法可以更好地演示它们的作用:

Cons { headL :: a, tailL :: List a }

例如,类型为List Int的值,如果是使用Cons构造函数构造的,则具有两个字段:一个Int和一个List Int。如果使用Nil构造函数构造,则没有任何字段。

好的,最后还有一点混淆。似乎a被视为第一个字段的值。请参见我编辑过的问题。谢谢。 - Charlie Flowers

3

5是错误的,我建议用6来替换它们:

Cons{1} a{2} (List a){3}是一个构造函数,称为Cons({1}之前的部分),用于类型为List a的值(数据List a部分),需要两个值:一个a类型的值({1}和{2}之间的部分)和一个List a类型的值({2}和{3}之间的部分)。

为了帮助你解决明显的困惑:在Haskell中,你几乎不必给出显式的类型参数——类型推断可以从你的值中推断出类型。因此,在某种意义上,当你将一个值传递给函数或构造函数时,你也指定了一种类型,即传递值的类型。


好的,最后还有一个困惑。似乎a被视为第一个字段的值。请参见我编辑过的问题。谢谢。 - Charlie Flowers

2
当我输入“Cons 0 Nil”时,它使用“Cons”值构造函数来创建List的实例。从0中得知类型参数是“Integer”,目前为止没有混淆。
然而,它还确定了Cons的第一个字段的值为0,但是它并没有确定第二个字段的值,只确定第二个字段的类型是“List Integer”。
不,它确定第二个字段的值是Nil。根据您的定义,Nil是类型List a的一个值。因此,Cons 0 Nil也是这种类型的值。在Cons 1 it中,第二个字段的值是it,即Cons 0 Nil。这正是REPL所显示的:Cons 1 (Cons 0 Nil)

1
我开始明白了,从你的回答和其他人的回答中。值必须始终是显式提供的(至少据我所知:)。可以推断出类型。因此,“a”和“List a”什么都没有指定,只有类型。当我调用它时,我会为字段一和字段二(显式地)提供一个值。 - Charlie Flowers

1

我看了你编辑过的问题。

当我使用 Cons 值构造函数创建实例时,这些实例将第一个 'a' 解释为表示“在此处传递值”。

在 "Cons a (List a)" 中,"a" 和 "List a" 都是类型。我不明白 "value" 与此有何关系。

当我输入“Cons 0 Nil”时,它使用“Cons”值构造函数创建 List 的实例。从 0 中,它学到类型参数是“Integer”。到目前为止,没有混淆。

然而,它还确定了 Cons 的第一个字段的值为 0。但它对第二个字段的值没有任何确定... 它只确定第二个字段的类型为“List Integer”。

第二个字段的值是 Nil

所以我的问题是,为什么第一个字段中的“a”表示“此字段的类型为'a',其值为'a'”,而第二个字段中的“a”仅表示“此字段的类型为'List of a'”?
第一个字段中的“a”表示“此字段的类型为'a'”。第二个字段中的“List a”表示“此字段的类型为'List a'”。在上面的“Cons 0 Nil”中,“a”被推断为“整数”。因此,“Cons a(List a)”变为“Cons Integer(List Integer)”。0是整数类型的值。Nil是“List Integer”类型的值。
我不明白你所说的“此字段的值为'a'”是什么意思。'a'是一种类型变量;它与值有什么关系?

谢谢。您的回复以及其他几个回复帮助我理解了这个问题。在“Cons a(List a)”中,“a”和“List a”都只是类型。当我稍后键入“Cons 0 Nil”时,它会推断出a是整数(现在足够接近),并将第一个字段设置为零,第二个字段设置为Nil。我错过了这个事实,即虽然它会推断类型,但它不会推断值...这些值必须明确提供。 - Charlie Flowers

0

如果你还在关注这个线程,我想给你一些额外的“帮助”。Haskell有一些约定俗成的规则,会让其他人对该如何做事情产生困惑。在Haskell中,参数化类型是如此普遍被接受,以至于它通常被认为是一种类型级别的函数。同样,值构造函数被认为是“特殊”的函数,除了“获取一个值(或更多)并产生一个值作为结果”之外,它们还允许模式匹配。

Haskell的另一个“有趣”特性是,即使参数用括号括起来,它也不会明确地(或隐含地)评估函数的参数。让我稍微改一下说法:Haskell函数不会在其他参数之前评估括号中的参数。参数只是为了分组而放在括号中,而不是为了让它们首先被评估。Haskell将参数分配给(“应用”)函数的优先级高于任何其他操作——甚至高于其自己参数的隐式函数应用。这就是为什么Cons构造函数在第二个参数(List a)周围有括号的原因——告诉编译器Cons有两个参数,而不是三个。括号只是用于分组,而不是用于优先级!

作为一个旁支话题,要小心F#中的类型。由于F#起源于ML,其参数化类型将参数放在前面 - int list,而不是后面的List Int!Haskell则相反,因为它与Haskell函数的方式相同 - 首先是函数,然后是函数的参数。这鼓励了一种常见的使用模式,并解释了为什么Haskell类型和值构造函数大写 - 提醒您正在处理与类型/类相关的事物。

好了,我说完了;谢谢你让我在你的财产上放这个巨大的文本墙...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接