Haskell 类型与数据构造函数

我从 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,这种状态可以使用各种参数来构建?如果是这样,那么第二个例子又是怎样的呢?

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

52553 次浏览

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

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

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

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

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

至于第三种方式,我不认为它需要适合于前一种类型。这只是定义哈斯克尔数据的另一种方式。

这是我作为一个初学者的拙见。

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

从最简单的例子开始:

data Color = Blue | Green | Red

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

当您需要创建某种类型的值时,可以使用数据构造函数。例如:

myFavoriteColor :: Color
myFavoriteColor = Green

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

当你需要创建某种类型的 ABc0时,会使用型别构造器。在书写签名时通常是这种情况:

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型别构造器来获取数据构造函数返回的“有效”类型的。这触及到了 种类的概念,你可能想在喝一两杯咖啡的时候读到它。

现在我们了解了什么是数据构造函数和类型构造函数,以及数据构造函数如何将其他值作为参数,类型构造函数如何将其他类型作为参数。高温。

正如其他人指出的那样,多态性在这里并没有那么糟糕,让我们看看另一个您可能已经熟悉的例子:

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

所以在这种情况下,aInt,但是它也适用于任何其他类型。事实上,您可以让我们的函数为每种类型的列表工作(即使不改变实现) :

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声明中,型别构造器是等号左边的值。数据构造函数是等号右边的数字。在需要类型的地方使用类型构造函数,在需要值的地方使用数据构造函数。

数据构造函数

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

data Colour = Red | Green | Blue

这里,我们有三个数据构造函数。Colour是一个类型,而 Green是一个包含 Colour类型值的构造函数。类似地,RedBlue都是构造 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类型的 构造了一个值。数据构造函数要么像变量一样包含一个值,要么使用其他值作为参数并创建一个新的 价值。如果您已经完成了以前的编程,那么这个概念对您来说应该不是很陌生。

中场休息

如果您想构造一个二叉树来存储 Strings,您可以想象这样做

data SBTree = Leaf String
| Branch String SBTree SBTree

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

我们还看到两个数据构造函数都有一个 String参数——这是它们要存储在树中的 String。

但是!如果我们也希望能够存储 Bool,那么我们必须创建一个新的二叉树。它可能看起来像这样:

data BBTree = Leaf Bool
| Branch Bool BBTree BBTree

类型构造函数

SBTreeBBTree都是类型构造函数。但有一个突出的问题。你看到他们有多相似了吗?这表明你确实需要一个参数。

所以我们可以这样做:

data BTree a = Leaf a
| Branch a (BTree a) (BTree a)

现在我们引入一个 类型变量 a作为型别构造器的参数。在这个声明中,BTree变成了一个函数。它接受一个 类型作为参数,并返回一个新的 类型

在这里,重要的是要考虑 混凝土类型(例如 Int[Char]Maybe Bool)和 型别构造器功能之间的区别,混凝土类型是一种可以被赋值给程序的类型,而 型别构造器功能需要提供一种类型才能被赋值。值不能是“ list”类型的,因为它需要是“ list 某种东西”。本着同样的精神,值永远不能是“二叉树”类型,因为它需要是“存储 什么的的二叉树”。

如果我们将 Bool作为参数传入 BTree,它将返回类型 BTree Bool,这是一个存储 Bool的二叉树。用类型 Bool替换类型变量 a的每次出现,您就可以亲眼看到它是如何正确的。

如果需要,可以将 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 IntMaybe a的列表。这是因为 Maybe是一个型别构造器函数,但列表需要包含具体类型的值。Maybe IntMaybe a是具体类型(或者如果你愿意,可以调用返回具体类型的型别构造器函数)

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

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

在哈斯克尔,你可以同时做到这两点!

为什么它被称为“代数”? 好吧,书呆子们谈论“和类型”和“产品类型”。例如:

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

(很明显,那里有一些合适的田地。)你不能用正常的编程语言做这样的事情,这就是为什么大多数人不习惯它。