我正在从 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
,它可以采用各种参数来构建?如果是这样,第二个例子如何适应?
本质上,我正在寻找一种统一上述三个代码示例/结构的解释。
Car
既是类型构造函数(在 =
的左侧)又是数据构造函数(在右侧)。在第一个示例中,Car
类型构造函数不带参数,在第二个示例中,它需要三个参数。在这两个示例中,Car
数据构造函数采用三个参数(但这些参数的类型在一种情况下是固定的,而在另一种情况下是参数化的)。
Car :: String -> String -> Int -> Car
) 来构建类型 Car
的数据。第二种是简单地使用一个数据构造函数 (Car :: a -> b -> c -> Car a b c
) 来构建类型 Car a b c
的数据。
在 data
声明中,类型构造函数是等号左侧的东西。 数据构造函数是等号右边的东西。在需要类型的地方使用类型构造函数,在需要值的地方使用数据构造函数。
数据构造函数
为简单起见,我们可以从表示颜色的类型的示例开始。
data Colour = Red | Green | Blue
在这里,我们有三个数据构造函数。 Colour
是一个类型,Green
是一个包含 Colour
类型值的构造函数。同样,Red
和 Blue
都是构造 Colour
类型值的构造函数。不过,我们可以想象给它加香料!
data Colour = RGB Int Int Int
我们仍然只有 Colour
类型,但 RGB
不是一个值——它是一个接受三个 Int 并返回一个值的函数! 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 作为其参数,并返回一个新的 type。
重要的是要考虑具体类型(示例包括 Int、[Char] 和 Maybe Bool)之间的区别,具体类型是可以在程序中分配给值的类型,而类型构造函数需要提供类型以能够分配给一个值。值永远不能是“列表”类型,因为它必须是“某物的列表”。本着同样的精神,一个值永远不能是“二叉树”类型,因为它需要是一个“存储某些东西的二叉树”。
如果我们将 Bool
作为参数传递给 BTree
,它会返回类型 BTree Bool
,它是一个存储 Bool
的二叉树。将每次出现的类型变量 a
替换为类型 Bool
,您就可以亲眼看到它的真实性。
如果您愿意,您可以将 BTree
视为带有 kind 的函数
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
是具体类型(或者,如果您愿意,可以调用返回具体类型的类型构造函数。)
Haskell 具有代数数据类型,其他语言很少有。这也许就是让你感到困惑的地方。
在其他语言中,您通常可以创建一个“记录”、“结构”或类似的,其中包含一堆包含各种不同类型数据的命名字段。您有时也可以创建一个“枚举”,它有一组(小)固定的可能值(例如,您的 Red
、Green
和 Blue
)。
在 Haskell 中,您可以同时将这两者结合起来。奇怪,但真实!
为什么叫“代数”?好吧,书呆子谈论“总和类型”和“产品类型”。例如:
data Eg1 = One Int | Two String
Eg1
值基本上是要么 整数或字符串。所以所有可能的 Eg1
值的集合是所有可能的整数值和所有可能的字符串值的集合的“总和”。因此,书呆子将 Eg1
称为“求和类型”。另一方面:
data Eg2 = Pair Int String
每个 Eg2
值都由 both 一个整数和一个字符串组成。所以所有可能的 Eg2
值的集合是所有整数的集合和所有字符串的集合的笛卡尔积。这两个集合“相乘”在一起,所以这是一个“产品类型”。
Haskell 的代数类型是乘积类型的总和类型。您为构造函数提供多个字段来创建产品类型,并且您有多个构造函数来计算(产品的)总和。
举例说明这可能有用的原因,假设您有一些以 XML 或 JSON 格式输出数据的东西,并且它需要一个配置记录 - 但显然,XML 和 JSON 的配置设置完全不同。所以你可能会做这样的事情:
data Config = XML_Config {...} | JSON_Config {...}
(显然,其中有一些合适的字段。)你不能用普通的编程语言做这样的事情,这就是为什么大多数人不习惯它的原因。
union
s,带有标签规则。 :)
union
时,人们都会以“谁他妈用过那个??”的表情看着我。 ;-)
union
。请不要让它听起来没有必要,因为事实并非如此。
从最简单的情况开始:
data Color = Blue | Green | Red
这定义了一个不带参数的“类型构造函数”Color
- 它具有三个“数据构造函数”,Blue
、Green
和 Red
。没有一个数据构造函数接受任何参数。这意味着存在 Color
类型的三种:Blue
、Green
和 Red
。
当您需要创建某种类型的值时,使用数据构造函数。喜欢:
myFavoriteColor :: Color
myFavoriteColor = Green
使用 Green
数据构造函数创建一个值 myFavoriteColor
- 并且 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。
->
的东西。
data Color a = Red a
中的 a
。 a
是任意类型的占位符。不过,您可以在普通函数中使用相同的方法,例如 (a, b) -> a
类型的函数采用两个值(类型为 a
和 b
)的元组并产生第一个值。这是一个“通用”函数,因为它不指定元组元素的类型——它只指定函数产生与第一个元组元素相同类型的值。
Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.
这很有帮助。
正如其他人指出的那样,多态在这里并不是那么有用。让我们看另一个您可能已经熟悉的示例:
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
,它会像以前一样工作(当然访问公司数据的函数需要更改)。
data Color = Blue ; data Bright = Color
之类的东西?我在ghci中试了一下,好像类型构造函数中的Color和Bright定义中的Color数据构造函数没有关系。只有 2 个 Color 构造函数,一个是 Data,另一个是 Type。
data
或 newtype
(例如 data Bright = Bright Color
)“包装”现有类型,或者您可以使用 type
来定义同义词(例如 type Bright = Color
)。
第二个有“多态性”的概念。
a b c
可以是任何类型。例如,a
可以是 [String]
,b
可以是 [Int]
,c
可以是 [Char]
。
第一个类型是固定的:company 是 String
,model 是 String
,year 是 Int
。
Car 示例可能没有显示使用多态性的重要性。但是想象一下你的数据是列表类型的。列表可以包含 String, Char, Int ...
在这些情况下,您将需要定义数据的第二种方式。
至于第三种方式,我认为它不需要适合以前的类型。这只是在 Haskell 中定义数据的另一种方式。
这是我自己作为初学者的拙见。
顺便说一句:确保你训练好你的大脑并且对此感到舒服。是后面理解 Monad 的关键。
这与类型有关:在第一种情况下,您设置类型 String
(用于公司和型号)和 Int
用于年份。在第二种情况下,你的更通用。 a
、b
和 c
可能与第一个示例中的类型完全相同,或则完全不同。例如,将年份作为字符串而不是整数可能会很有用。如果你愿意,你甚至可以使用你的 Color
类型。
data Colour = Red | Green | Blue
中“我们根本没有任何构造函数”的说法是完全错误的。类型构造函数和数据构造函数不需要带参数,参见例如 haskell.org/haskellwiki/Constructor,它指出在data Tree a = Tip | Node a (Tree a) (Tree a)
中,“有两个数据构造函数,Tip 和 Node”。-XEmptyDataDecls
) 可以让您做到这一点。正如您所说,由于没有该类型的值,因此函数f :: Int -> Z
可能永远不会返回(因为它会返回什么?)但是它们对 when you want types but don't really care about values 很有用。:k Z
,它给了我一颗星。