Haskell使人们能够使用类型构造函数和数据构造函数构造代数数据类型。例如,
data Circle = Circle Float Float Float
并且我们被告知这个数据构造函数(右边的圆)是一个在给数据时构造一个圆的函数,例如x,y,半径。
Circle :: Float -> Float -> Float -> Circle
我的问题是:
此功能实际上构造了什么?
我们可以定义构造函数吗?
我见过智能构造函数,但它们似乎是最终会调用常规构造函数的额外功能。
从OO的背景来看,构造函数当然具有必要的规范。在Haskell中,它们似乎是系统定义的。
答案 0 :(得分:15)
在Haskell中,数据构造函数基本上不通过法定命令来创建值,而无需考虑基础实现。 “'有一个Circle
',程序员说,那里有一个Circle
。”问Circle 1 2 3
产生了什么,就像问字面量1
在其中创造了什么一样。 Python或Java。
无效构造函数更接近您通常认为的文字。 Boolean
类型在字面上是 定义为
data Boolean = True | False
其中True
和False
是数据构造函数,而不是Haskell语法定义的文字。
数据类型也是构造函数的定义;因为除了构造函数名称及其参数之外,值实际上没有任何其他内容,因此只需将其声明为 即可。通过调用带有3个参数的数据构造函数Circle
来创建类型Circle
的值。
所谓的“智能构造函数”只是一个调用数据构造函数的函数,也许还有其他逻辑来限制可以创建哪些实例。例如,考虑一个简单的Integer
周围的包装器:
newtype PosInteger = PosInt Integer
构造函数为PosInt
;一个聪明的构造函数可能看起来像
mkPosInt :: Integer -> PosInteger
mkPosInt n | n > 0 = PosInt n
| otherwise = error "Argument must be positive"
对于mkPosInt
,无法使用非正参数创建PosInteger
值,因为只有正参数实际上会调用数据构造函数。当模块导出数据时,智能构造函数而不是数据构造函数最有意义,因此典型用户无法创建任意实例(因为数据构造函数不在模块外部)。
答案 1 :(得分:12)
好问题。如您所知,给定定义:
data Foo = A | B Int
这定义了一个具有(空)类型构造函数Foo
和两个数据构造函数A
和B
的类型。
这些数据构造函数中的每一个在完全应用时(对于A
来说,没有参数,对于Int
来说,只有一个B
参数)会构造一个type值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构造函数与OO构造函数有很大不同,但这并不奇怪,因为Haskell值与OO对象有很大不同。在OO语言中,通常可以提供构造函数,该函数在构建对象时进行一些处理,因此在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构造函数运行一些基础函数来“构造”一个可能涉及一些很酷的处理和字段值派生的对象,还不如将Haskell构造函数视为附加到值的被动标记(这也可能是包含零个或多个其他值,具体取决于构造函数的类型。
因此,在您的示例中,Circle 10 20 5
不会通过运行某些函数来“构造” Circle
类型的对象。它直接创建一个带标签的对象,该对象在内存中将类似于:
<Circle tag>
<Float value 10>
<Float value 20>
<Float value 5>
(或者您至少可以假装这就是它在内存中的样子)。
在Haskell中,最接近OO构造函数的地方是使用智能构造函数。如您所述,最终,智能构造函数只调用常规构造函数,因为这是创建给定类型的值的唯一方法。无论您构建哪种类型的奇异智能构造函数来创建Circle
,其构造值都必须看起来像:
<Circle tag>
<some Float value>
<another Float value>
<a final Float value>
您需要使用普通的旧Circle
构造函数调用来构建。智能构造函数返回的其他值仍然是Circle
。这就是Haskell的工作方式。
有帮助吗?
答案 2 :(得分:7)
我将以某种round回的方式回答这个问题,并举一个我希望说明我观点的例子,那就是Haskell decouples 在OOP的概念下耦合的几个不同的想法一类”。了解这一点将帮助您以较少的难度将您的经验从OOP转换为Haskell。 OOP伪代码中的示例:
class Person {
private int id;
private String name;
public Person(int id, String name) {
if (id == 0)
throw new InvalidIdException();
if (name == "")
throw new InvalidNameException();
this.name = name;
this.id = id;
}
public int getId() { return this.id; }
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
}
在Haskell中:
module Person
( Person
, mkPerson
, getId
, getName
, setName
) where
data Person = Person
{ personId :: Int
, personName :: String
}
mkPerson :: Int -> String -> Either String Person
mkPerson id name
| id == 0 = Left "invalid id"
| name == "" = Left "invalid name"
| otherwise = Right (Person id name)
getId :: Person -> Int
getId = personId
getName :: Person -> String
getName = personName
setName :: String -> Person -> Either String Person
setName name person = mkPerson (personId person) name
注意:
Person
类已转换为一个 module 模块,该模块恰好导出具有相同名称的数据类型- types (用于域表示)和不变式)与模块分离(用于命名空间和代码组织)。
在id
定义中指定为name
的字段private
和class
被转换为{{ 1}}的定义,因为在Haskell中,它们通过从data
模块的导出列表中省略而被私有化,因此 definitions 和 visibility 是分离的。 / p>
该构造函数已转换为两部分:一个(Person
数据构造函数)仅用于初始化字段,而另一个(Person
)则执行验证-分配和初始化和验证是分离的。由于已导出mkPerson
类型,但未导出其构造函数,因此这是客户端构造Person
的唯一方法-这是“抽象数据类型”。
公共接口已转换为Person
模块导出的 函数,以及以前变异Person函数> setName
对象已成为一个函数,该函数返回一个恰好共享旧ID的Person
数据类型的新实例。 OOP代码有一个错误:它应该在Person
中检查setName
不变量; Haskell代码可以通过使用name != ""
智能构造函数来确保所有mkPerson
值在构造上都是有效的,从而避免了这种情况。因此,状态转换和验证也是解耦的-您只需要在构造值时检查不变式,因为此后它不会更改。
关于您的实际问题:
- 此功能实际上构造了什么?
数据类型的构造函数为值的标记和字段分配空间,设置构造函数用于创建值的标记,并将字段初始化为构造函数的参数。您无法覆盖它,因为该过程完全是机械过程,因此没有理由(以正常的安全代码)。这是语言和运行时的内部细节。
- 我们可以定义构造函数吗?
否-如果要执行其他验证以实施不变式,则应使用“智能构造函数”函数来调用较低级别的数据构造函数。由于Haskell值默认情况下是不可变的,因此可以通过构造使它们正确;也就是说,当您没有突变时,您无需强制要求所有状态过渡都是正确的,仅需正确构造所有状态自身即可。通常,您可以安排类型,以便甚至不需要智能构造函数。
关于生成的数据构造函数“函数”,您唯一可以更改的就是使用GADT使其类型签名更具限制性,以帮助在编译时强制执行更多不变式。另外,GADT还允许您进行现有量化,从而使您可以在运行时随身携带封装的/类型擦除的信息,就像OOP vtable一样精确地。是另一种在Haskell中解耦但又在典型的OOP语言中耦合的东西。
长话短说(太晚了),您可以做所有相同的事情,只是以不同的方式安排它们,因为Haskell在单独的正交语言功能下提供了OOP类的各种功能。