我正在从learnyouahaskell.com学习Haskell。我无法理解类型构造函数和数据构造函数。例如,我真的不明白这个之间的区别:
data Car = Car { company :: String
, model :: String
, year :: Int
} deriving (Show)
和此:
data Car a b c = Car { company :: a
, model :: b
, year :: c
} deriving (Show)
我知道第一个只是使用一个构造函数(Car
)来构建类型为Car
的数据。我真的不明白第二个。
此外,如何定义数据类型如下:
data Color = Blue | Green | Red
适合所有这些吗?
据我所知,第三个示例(Color
)是一种类型,可以分为三种状态:Blue
,Green
或Red
。但这与我理解前两个示例的方式存在冲突:类型Car
只能处于一种状态Car
,它可以构建各种参数吗?如果是这样,第二个例子如何适应?
基本上,我正在寻找一个统一上述三个代码示例/结构的解释。
答案 0 :(得分:207)
在data
声明中,类型构造函数是等号左侧的东西。 数据构造函数是等号右侧的东西。您可以使用需要类型的类型构造函数,并使用期望值的数据构造函数。
为简单起见,我们可以从表示颜色的类型示例开始。
data Colour = Red | Green | Blue
这里,我们有三个数据构造函数。 Colour
是一种类型,Green
是包含类型Colour
的值的构造函数。同样,Red
和Blue
都是构造Colour
类型值的构造函数。我们可以想象它会加油!
data Colour = RGB Int Int Int
我们仍然只有Colour
类型,但RGB
不是值 - 它是一个带三个Ints并且返回值的函数! RGB
的类型为
RGB :: Int -> Int -> Int -> Colour
RGB
是一个数据构造函数,它是一个以值为参数的函数,然后使用它们构造一个新值。如果您已经完成了任何面向对象的编程,那么您应该认识到这一点。在OOP中,构造函数也将一些值作为参数并返回一个新值!
在这种情况下,如果我们将RGB
应用于三个值,我们会得到一个颜色值!
Prelude> RGB 12 92 27
#0c5c1b
我们通过应用数据构造函数构造了Colour
类型的值。数据构造函数包含类似变量的值,或者将其他值作为其参数,并创建新的值。如果你以前做过编程,这个概念对你来说应该不会很奇怪。
如果你想构建一个存储String
的二叉树,你可以想象做类似的事情
data SBTree = Leaf String
| Branch String SBTree SBTree
我们在这里看到的是一个类型SBTree
,它包含两个数据构造函数。换句话说,有两个函数(即Leaf
和Branch
)将构造SBTree
类型的值。如果您不熟悉二叉树如何工作,只需挂在那里。您实际上并不需要知道二叉树是如何工作的,只是这个以某种方式存储String
。
我们还看到两个数据构造函数都采用String
参数 - 这是它们将存储在树中的字符串。
但是!如果我们还希望能够存储Bool
,我们必须创建一个新的二叉树。它可能看起来像这样:
data BBTree = Leaf Bool
| Branch Bool BBTree BBTree
SBTree
和BBTree
都是类型构造函数。但这是一个明显的问题。你看他们有多相似吗?这表明你真的想要一个参数。
所以我们可以这样做:
data BTree a = Leaf a
| Branch a (BTree a) (BTree a)
现在我们引入一个类型变量 a
作为类型构造函数的参数。在此声明中,BTree
已成为一种功能。它需要 type 作为参数,并返回一个新的类型。
在此重要的是要考虑具体类型(示例包括
Int
,[Char]
和Maybe Bool
)之间的区别,这种类型可以是分配给程序中的值,以及类型构造函数,您需要提供一个类型才能分配给值。值永远不能是类型" list",因为它需要是"列出的某些内容"。本着同样的精神,一个值永远不会是类型"二叉树",因为它需要是一个"二叉树存储某事"。
如果我们将Bool
作为BTree
的参数传入,则返回类型BTree Bool
,它是存储Bool
的二叉树。将类型变量a
的每次出现替换为类型Bool
,您可以自己查看它是如何真实的。
如果您愿意,可以使用种类
查看BTree
作为函数
BTree :: * -> *
种类有点像类型 - *
表示具体类型,因此我们说BTree
是从具体类型到具体类型。
回到这里,注意相似之处。
数据构造函数是"函数"需要0或更多值并返回一个新值。
类型构造函数是"函数"需要0个或更多类型并返回一个新类型。
如果我们想要略微变化我们的值,那么带参数的数据构造函数很酷 - 我们将这些变量放在参数中,让创建值的人决定他们将要放入什么参数。在同样的意义上,键入构造函数如果我们想要我们的类型略有变化,参数很酷!我们将这些变化作为参数,让创建该类型的人决定他们将要放入哪些参数。
作为主页,我们可以考虑Maybe a
类型。它的定义是
data Maybe a = Nothing
| Just a
这里,Maybe
是一个返回具体类型的类型构造函数。 Just
是一个返回值的数据构造函数。 Nothing
是包含值的数据构造函数。如果我们查看Just
的类型,我们会看到
Just :: a -> Maybe a
换句话说,Just
采用a
类型的值并返回类型Maybe a
的值。如果我们查看Maybe
的类型,我们会看到
Maybe :: * -> *
换句话说,Maybe
采用具体类型并返回具体类型。
再一次!具体类型和类型构造函数之间的区别。如果您尝试执行
,则无法创建Maybe
的列表
[] :: [Maybe]
你会得到一个错误。但是,您可以创建Maybe Int
或Maybe a
的列表。这是因为Maybe
是一个类型构造函数,但是列表需要包含具体类型的值。 Maybe Int
和Maybe a
是具体类型(或者,如果需要,可以调用返回具体类型的构造函数)。
答案 1 :(得分:40)
Haskell有代数数据类型,其他语言很少。这可能让你感到困惑。
在其他语言中,您通常可以创建一个“记录”,“结构”或类似的东西,它有一堆包含各种不同类型数据的命名字段。您有时也可以制作“枚举”,其中包含一小组固定的可能值(例如,您的Red
,Green
和Blue
。
在Haskell中,您可以同时组合这两者。很奇怪,但确实如此!
为什么称它为“代数”?好吧,书呆子谈论“总和类型”和“产品类型”。例如:
data Eg1 = One Int | Two String
Eg1
值基本上是 整数或字符串。因此,所有可能的Eg1
值的集合是所有可能的整数值和所有可能的字符串值的集合的“总和”。因此,书呆子将Eg1
称为“和类型”。另一方面:
data Eg2 = Pair Int String
每个Eg2
值包含 整数和字符串。因此,所有可能的Eg2
值的集合是所有整数集的笛卡尔乘积和所有字符串的集合。这两组是“相乘”的,因此这是“产品类型”。
Haskell的代数类型是产品类型的总和类型。您为构造函数提供了多个字段来生成产品类型,并且您有多个构造函数来生成(产品)总和。
作为可能有用的原因的一个示例,假设您有一些输出数据为XML或JSON的东西,它需要一个配置记录 - 但显然,XML和JSON的配置设置完全不同。所以你可能做这样的事情:
data Config = XML_Config {...} | JSON_Config {...}
(显然有一些合适的字段。)你不能在普通的编程语言中做这样的事情,这就是为什么大多数人不习惯它。
答案 2 :(得分:24)
从最简单的情况开始:
data Color = Blue | Green | Red
这定义了一个不带参数的“类型构造函数”Color
- 它有三个“数据构造函数”,Blue
,Green
和Red
。没有数据构造函数采用任何参数。这意味着Color
类型有三种:Blue
,Green
和Red
。
当您需要创建某种值时,将使用数据构造函数。像:
myFavoriteColor :: Color
myFavoriteColor = Green
使用myFavoriteColor
数据构造函数创建值Green
,而myFavoriteColor
的类型为Color
,因为这是数据构造函数生成的值的类型。
当您需要创建某种类型的类型时,将使用类型构造函数。编写签名时通常就是这种情况:
isFavoriteColor :: Color -> Bool
在这种情况下,您正在调用Color
类型构造函数(不带参数)。
还在我身边吗?
现在,假设您不仅要创建红色/绿色/蓝色值,还要指定“强度”。比如,介于0和256之间的值。您可以通过向每个数据构造函数添加一个参数来实现,因此最终得到:
data Color = Blue Int | Green Int | Red Int
现在,三个数据构造函数中的每一个都采用类型为Int
的参数。类型构造函数(Color
)仍然不接受任何参数。所以,我最喜欢的颜色是深绿色,我可以写
myFavoriteColor :: Color
myFavoriteColor = Green 50
同样,它调用Green
数据构造函数,我得到类型为Color
的值。
想象一下,如果你不想决定人们如何表达一种颜色的强度。有些人可能想要像我们刚才那样的数值。其他人可能没什么问题只有布尔表示“明亮”或“不那么明亮”。对此的解决方案是不在数据构造函数中硬编码Int
,而是使用类型变量:
data Color a = Blue a | Green a | Red a
现在,我们的类型构造函数接受一个参数(我们只调用a
的另一个类型!),并且所有数据构造函数将采用该类型a
的一个参数(值!)。你可以拥有
myFavoriteColor :: Color Bool
myFavoriteColor = Green False
或
myFavoriteColor :: Color Int
myFavoriteColor = Green 50
注意我们如何使用参数(另一种类型)调用Color
类型构造函数来获取将由数据构造函数返回的“有效”类型。这触及了kinds的概念,您可能想要阅读一两杯咖啡。
现在我们弄清楚了数据构造函数和类型构造函数是什么,以及数据构造函数如何将其他值作为参数和类型构造函数可以将其他类型作为参数。 HTH。
答案 3 :(得分:5)
第二个中有“多态”的概念。
a b c
可以是任何类型。例如,a
可以是[String]
,b
可以是[Int]
c
可以是[Char]
。
虽然第一个类型已修复:公司为String
,模型为String
,年份为Int
。
Car示例可能没有显示使用多态的重要性。但想象一下,您的数据属于列表类型。列表可以包含String, Char, Int ...
在这些情况下,您需要第二种方法来定义数据。
至于第三种方式,我认为它不需要适合以前的类型。这只是在Haskell中定义数据的另一种方式。
这是我自己作为初学者的拙见。
顺便说一下:确保你训练好大脑并对此感到舒服。这是了解Monad的关键。答案 4 :(得分:5)
正如其他人所指出的那样,多态性在这里并没有那么糟糕。让我们看看你可能已经熟悉的另一个例子:
Maybe a = Just a | Nothing
此类型有两个数据构造函数。 Nothing
有点无聊,它不包含任何有用的数据。另一方面,Just
包含a
的值 - a
可能具有的任何类型。让我们编写一个使用这种类型的函数,例如获得Int
列表的头部,如果有的话(我希望你同意这比抛出错误更有用):
maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x
> maybeHead [1,2,3] -- Just 1
> maybeHead [] -- None
因此,在这种情况下a
是Int
,但它对任何其他类型都有效。实际上,您可以使我们的函数适用于每种类型的列表(即使不更改实现):
maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x
另一方面,您可以编写仅接受某种Maybe
类型的函数,例如
doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing
长话短说,使用多态性,您可以灵活地使用不同类型的值来处理自己的类型。
在您的示例中,您可能会在某个时候决定String
不足以识别公司,但它需要有自己的类型Company
(其中包含国家,地址等其他数据,退帐等)。您的第一次Car
实施需要更改为使用Company
而不是String
作为其第一个值。您的第二个实现很好,您将其用作Car Company String Int
并且它将像以前一样工作(当然,需要更改访问公司数据的函数)。
答案 5 :(得分:1)
关于类型:在第一种情况下,您为年份设置类型String
(公司和模型)和Int
。在第二种情况下,您更通用。 a
,b
和c
可能与第一个示例中的类型相同,或完全不同。例如,将年份作为字符串而不是整数给出可能是有用的。如果您愿意,您甚至可以使用Color
类型。