133

我正在从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) 是一种可以处于三种状态的类型BlueGreenRed. 但这与我对前两个示例的理解相冲突:类型Car只能处于一种状态,Car可以采用各种参数来构建吗?如果是这样,第二个例子如何适应?

本质上,我正在寻找一种统一上述三个代码示例/结构的解释。

4

6 回答 6

249

data声明中,类型构造函数是等号左侧的东西。数据构造函数是等号右侧的东西。在需要类型的地方使用类型构造函数,在需要值的地方使用数据构造函数。

数据构造函数

为简单起见,我们可以从表示颜色的类型的示例开始。

data Colour = Red | Green | Blue

在这里,我们有三个数据构造函数。Colour是一个类型,并且Green是一个包含 type 值的构造函数Colour。同样,RedandBlue都是构造类型值的构造函数Colour。不过,我们可以想象给它加香料!

data Colour = RGB Int Int Int

我们仍然只有 type Colour,但RGB不是 value ——它是一个接受三个 Int 并返回一个值的函数!RGB有类型

RGB :: Int -> Int -> Int -> Colour

RGB是一个数据构造函数,它是将一些作为其参数的函数,然后使用这些值构造一个新值。如果你做过任何面向对象的编程,你应该认识到这一点。在 OOP 中,构造函数也将一些值作为参数并返回一个新值!

在这种情况下,如果我们应用RGB三个值,我们会得到一个颜色值!

Prelude> RGB 12 92 27
#0c5c1b

我们通过应用数据构造函数构造了一个类型的值。Colour数据构造函数要么像变量一样包含一个值,要么将其他值作为其参数并创建一个新。如果你以前做过编程,这个概念对你来说应该不会很陌生。

中场休息

如果你想构建一个二叉树来存储Strings,你可以想象做类似的事情

data SBTree = Leaf String
            | Branch String SBTree SBTree

我们在这里看到的是一个SBTree包含两个数据构造函数的类型。换句话说,有两个函数(即LeafBranch)将构造该SBTree类型的值。如果您不熟悉二叉树的工作原理,请坚持下去。您实际上不需要知道二叉树是如何工作的,只需要知道二叉树String以某种方式存储 s 即可。

我们还看到两个数据构造函数都带有一个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已经变成了一个函数。它接受一个类型作为参数,并返回一个新类型

这里重要的是要考虑具体类型示例包括Int和赋值给一个值。值永远不能是“列表”类型,因为它必须是“某物的列表”。本着同样的精神,一个值永远不能是“二叉树”类型,因为它需要是一个“存储某些东西的二叉树”。[Char]Maybe Bool

例如,如果我们将 sBool作为参数传入BTree,它会返回 type ,它是存储sBTree Bool的二叉树。Bool用 type 替换每次出现的 type 变量aBool你可以自己看看它是如何正确的。

如果您愿意,您可以将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接受一个具体类型并返回一个具体类型。

再次!具体类型和类型构造函数之间的区别。您无法创建Maybes 列表 - 如果您尝试执行

[] :: [Maybe]

你会得到一个错误。但是,您可以创建Maybe Int或的列表Maybe a。这是因为Maybe它是一个类型构造函数,但列表需要包含具体类型的值。Maybe Int并且Maybe a是具体类型(或者,如果您愿意,可以调用返回具体类型的类型构造函数。)

于 2013-08-13T09:49:52.497 回答
47

Haskell 具有代数数据类型,其他语言很少有。这也许就是让你感到困惑的地方。

在其他语言中,您通常可以创建一个“记录”、“结构”或类似的,其中包含一堆包含各种不同类型数据的命名字段。您有时也可以进行“枚举”,它具有(小)一组固定的可能值(例如,您的RedGreenBlue

在 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 {...}

(显然,其中有一些合适的字段。)你不能用普通的编程语言做这样的事情,这就是为什么大多数人不习惯它的原因。

于 2013-08-13T17:34:45.160 回答
28

从最简单的情况开始:

data Color = Blue | Green | Red

这定义了一个不带参数的“类型构造函数” Color——它具有三个“数据构造函数” BlueGreenRed。没有一个数据构造函数接受任何参数。这意味着有: 和Color三种Blue类型。GreenRed

当您需要创建某种类型的值时,使用数据构造函数。喜欢:

myFavoriteColor :: Color
myFavoriteColor = Green

myFavoriteColor使用Green数据构造函数创建一个值- 并且myFavoriteColor将是类型Color,因为这是数据构造函数生成的值的类型。

当您需要创建某种类型时,使用类型构造函数。写签名时通常是这种情况:

isFavoriteColor :: Color -> Bool

在这种情况下,您正在调用Color类型构造函数(不带参数)。

还在我这儿?

现在,假设您不仅想创建红/绿/蓝值,而且还想指定“强度”。比如,一个介于 0 和 256 之间的值。您可以通过向每个数据构造函数添加一个参数来做到这一点,因此您最终得到:

data Color = Blue Int | Green Int | Red Int

现在,三个数据构造函数中的每一个都接受一个 type 的参数Int。类型构造函数 ( Color) 仍然不接受任何参数。所以,我最喜欢的颜色是深绿色,我可以写

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

再一次,它调用Green数据构造函数,我得到一个 type 的值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类型构造函数以获取将由数据构造函数返回的“有效”类型。这触及了您可能想在一两杯咖啡中阅读的种类的概念。

现在我们弄清楚了数据构造函数和类型构造函数是什么,以及数据构造函数如何将其他值作为参数,而类型构造函数可以将其他类型作为参数。HTH。

于 2013-08-13T09:34:34.600 回答
6

正如其他人指出的那样,多态在这里并不是那么有用。让我们看另一个您可能已经熟悉的示例:

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并且它会像以前一样工作(当然需要更改访问公司数据的功能)。

于 2013-08-13T09:43:52.630 回答
5

第二个有“多态性”的概念。

a b c可以是任何类型。例如,a可以是[String]b可以是[Int]c可以是[Char]

第一个类型是固定的:公司是 a String,型号是 a String,年份是Int

Car 示例可能没有显示使用多态性的重要性。但是想象一下你的数据是列表类型的。一个列表可以包含String, Char, Int ...在这些情况下,您将需要第二种方式来定义您的数据。

至于第三种方式,我认为它不需要适合以前的类型。这只是在 Haskell 中定义数据的另一种方式。

这是我自己作为初学者的拙见。

顺便说一句:确保你训练好你的大脑并且对此感到舒服。是后面理解 Monad 的关键。

于 2013-08-13T08:48:50.787 回答
2

这是关于类型的:在第一种情况下,您设置类型String(用于公司和型号)和Int年份。在第二种情况下,你的更通用。a, b, andc可能与第一个示例中的类型完全相同,或者完全不同。例如,将年份作为字符串而不是整数可能会很有用。如果你愿意,你甚至可以使用你的Color类型。

于 2013-08-13T08:46:22.650 回答