好问题。如您所知,根据定义:
data Foo = A | B Int
这个定义了一个带有(零元)类型构造器Foo
和两个数据构造器A
和B
的类型。
每一个数据构造器都会在被完整应用后(在A
的情况下不接受参数,在B
的情况下接受一个Int
参数),构建出一个Foo
类型的值。所以,当我写下:
a :: Foo
a = A
b :: Foo
b = B 10
a
和
b
这两个名称都绑定到类型为
Foo
的两个值。
因此,类型Foo
的数据构造函数构造类型为Foo
的值。
那么,什么是类型Foo
的值呢?首先,它们与任何其他类型的值都不同。其次,它们完全由它们的数据构造函数定义。对于每个数据构造函数和传递给该数据构造函数的一组不同参数的组合,类型Foo
都有一个独特的值,不同于Foo
的所有其他值。也就是说,如果两个类型为Foo
的值使用相同的数据构造函数给出了相同的参数集进行构造,则它们是相同的。 (此处的“相同”意味着与“相等”不同,在给定类型Foo
的情况下可能没有定义,但让我们不深入探讨这个问题。)
这也是Haskell中的数据构造函数与函数不同的地方。如果我有一个函数:
bar :: Int -> Bool
可能会出现bar 1
和bar 2
的值完全相同的情况。例如,如果通过以下方式定义了bar
:
bar n = n > 0
那么显而易见的是,bar 1
、bar 2
(以及 bar 3
)都是完全相同的 True
。无论对于其参数的不同取值,bar
的取值是否相同将取决于函数的定义。
相反,如果 Bar
是一个构造函数:
data BarType = Bar Int
那么Bar 1
和Bar 2
是相同的值的情况永远不会出现。按定义,它们将是不同的值(类型为BarType
)。
顺便提一下,认为构造函数只是一种特殊类型的函数是一种常见观点。我个人认为这种说法不准确并且容易引起混淆。尽管构造函数通常可以像函数一样使用(特别是在表达式中时它们的行为非常类似于函数),但在我看来,这种观点经不起仔细的审查--构造函数在语言的表面语法中以不同的方式表示(具有大写标识符),可以用于函数无法用于的上下文中(例如模式匹配),在编译代码中以不同的方式表示等等。
因此,当你问“我们是否可以定义构造函数”时,答案是“不”,因为没有构造函数。相反,像A
、B
、Bar
或Circle
这样的构造函数就是它们自己--与函数不同的东西(有时候具有一些特殊的额外属性,可以像函数一样使用),能够构造属于数据构造函数所属类型的值。
这使得Haskell的构造函数与面向对象的构造函数非常不同,但这并不奇怪,因为Haskell值与面向对象的对象非常不同。在面向对象语言中,您通常可以提供一个构造函数来处理对象的构建,因此在Python中,您可能会编写:
class Bar:
def __init__(self, n):
self.value = n > 0
然后在此之后:
bar1 = Bar(1)
bar2 = Bar(2)
我们有两个不同的对象bar1
和bar2
(满足bar1!= bar2
),它们配置了相同的字段值,并且在某种意义上是“相等”的。 这在某种程度上介于上面的情况之间,bar 1
和bar 2
创建了两个相同的值(即True
),以及Bar 1
和Bar 2
创建了两个不同的值,根据定义,这些值不可能以任何方式“相同”。
您永远无法在Haskell构造函数中出现此情况。 您应该将Haskell构造函数视为附加到值的被动标记(可能还包含零个或多个其他值,具体取决于构造函数的arity),而不是将其视为运行某些基础功能来“构造”对象的标签,其中可能涉及一些酷炫的处理和派生字段值。
因此,在您的示例中,Circle 10 20 5
并不会通过运行某些函数“构造”Circle
类型的对象。 它直接创建一个带有标记的对象,它在内存中看起来类似于:
<Circle tag>
<Float value 10>
<Float value 20>
<Float value 5>
(或者你至少可以假装这是内存中的样子)。
在Haskell中,最接近面向对象构造函数的方式是使用智能构造函数。正如您所指出的,最终智能构造函数只会调用常规构造函数,因为那是创建特定类型值的唯一方法。无论您构建什么样的奇怪智能构造函数来创建一个Circle
,它构造出的值看起来都必须像:
<Circle tag>
<some Float value>
<another Float value>
<a final Float value>
你需要使用普通的
Circle
构造函数来构建它。没有其他智能构造函数可以返回仍然是一个
Circle
的内容。这就是Haskell的工作原理。
有帮助吗?
data
声明。 - Will Ness