Haskell类型与数据构造器


124

我正在从learningyouahaskell.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)来构建type数据Car。我不太了解第二个。

另外,如何定义数据类型,如下所示:

data Color = Blue | Green | Red

适合所有这些吗?

从我的理解,第三个例子(Color)是可以在三种状态类型:BlueGreenRed。但这与我理解前两个示例的方式相冲突:类型是否Car只能处于一种状态Car,该状态可以采用各种参数来构建?如果是这样,第二个示例如何适合?

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


18
您的Car例子可能有点令人困惑,因为Car它既是类型构造函数(位于的左侧=)又是数据构造函数(位于右侧)。在第一个示例中,Car类型构造函数不接受任何参数,在第二个示例中,它接受三个参数。在这两个示例中,Car数据构造函数都使用三个参数(但是,这些参数的类型在一种情况下是固定的,而在另一种情况下是固定的)。
Simon Shine

首先是简单地使用一个数据构造器(Car :: String -> String -> Int -> Car)建立类型的数据Car。第二个简单的方法是使用一个数据构造器(Car :: a -> b -> c -> Car a b c)来建立类型的数据Car a b c
内斯

Answers:


228

data声明中,类型构造函数是等号左侧的内容。该数据构造(S)是在右手边的东西的等号。在需要类型的地方使用类型构造函数,在期望值的地方使用数据构造函数。

数据构造器

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

data Colour = Red | Green | Blue

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

data Colour = RGB Int Int Int

我们仍然只有type Colour,但RGB没有值-这是一个接受三个Ints并返回值的函数!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),这是一个可以在你的程序被分配到一个值类型和类型构造函数,你需要养活一个类型能成为分配给一个值。值永远不能是“列表”类型,因为它必须是“ 某物列表”。本着同样的精神,值永远不能是“二叉树”类型,因为它必须是“存储某些东西的二叉树”。

例如,如果我们传入,Bool作为其参数BTree,它将返回type BTree Bool,它是存储Bools 的二叉树。替换类型变量的每次出现a与类型Bool,你可以看到自己是如何的真实。

如果你愿意,你可以查看BTree与该功能

BTree :: * -> *

种类有点像类型- *表示具体类型,所以我们说的BTree是从具体类型到具体类型。

包起来

退后一步,注意相似之处。

  • 一个数据的构造是一个“功能”取0或多个,给你回一个新的价值。

  • 类构造函数是一个“功能”取0或更多类型的,给你回一个新的类型。

如果我们想要值的微小变化,则带有参数的数据构造函数很酷–我们将这些变化放入参数中,然后让创建值的人决定要放入哪些参数。同理,带参数的类型构造函数很酷如果我们希望我们的类型稍有变化!我们将这些变体作为参数,然后让创建类型的人决定要输入哪些参数。

案例研究

作为这里的房屋,我们可以考虑Maybe a类型。它的定义是

data Maybe a = Nothing
             | Just a

这里Maybe是一个返回具体类型的类型构造函数。Just是一个返回值的数据构造函数。Nothing是包含值的数据构造函数。如果我们看一下的类型Just,就会看到

Just :: a -> Maybe a

换句话说,Just采用type a的值并返回type 的值Maybe a。如果我们看一下Maybe,我们会看到

Maybe :: * -> *

换句话说,Maybe采用具体类型并返回具体类型。

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

[] :: [Maybe]

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


2
在第一个示例中,RED GREEN和BLUE都是不带参数的构造函数。
OllieB

3
关于data Colour = Red | Green | Blue“我们根本没有任何构造函数” 的说法是完全错误的。类型构造函数和数据构造函数不需要带参数,请参见例如haskell.org/haskellwiki/Constructor,它指出在中data Tree a = Tip | Node a (Tree a) (Tree a),“有两个数据构造函数,Tip和Node”。
Frerich Raabe 2013年

1
@CMCDragonkai你是绝对正确的!种类是“类型的类型”。连接类型和值的概念的常用方法称为依赖类型Idris是Haskell启发的依存类型语言。使用正确的GHC扩展名,您还可以在Haskell中接近依赖类型。(有人开玩笑说“ Haskell的研究是要弄清楚没有依赖类型我们可以得到的接近依赖类型。”)
kqr 2014年

1
@CMCDragonkai在标准的Haskell中实际上不可能有一个空的数据声明。但是有一个GHC扩展名(-XEmptyDataDecls),您可以执行此操作。就像您说的那样,由于没有该类型的值,因此函数f :: Int -> Z可能永远不会返回(因为它将返回什么?),但是它们在您需要类型但并不真正关心值时很有用。
kqr 2014年

1
真的不可能吗?我只是在GHC中尝试过,它运行起来没有错误。我不必加载任何GHC扩展,只需加载香草GHC。然后我可以写:k Z,它给了我一颗星。
CMCDragonkai 2014年

42

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

(显然,其中有一些合适的字段。)您无法使用普通的编程语言来完成类似的工作,这就是为什么大多数人不习惯的原因。


4
大!维基百科说,只有一件事,“它们可以...几乎可以用任何一种语言构造” 。:)在C / ++中,是union带有标签规范的。:)
内斯

5
是的,但是每次我提及时union,人们都会像“谁在使用?;-)
MathematicalOrchid

1
union在我的C生涯中,我已经看到很多使用过的东西。请不要让它听起来不必要,因为事实并非如此。
trueadjustr

26

从最简单的情况开始:

data Color = Blue | Green | Red

这定义了一个Color不带参数的“类型构造函数” -它具有三个“数据构造函数” BlueGreenRed。数据构造函数均未接受任何参数。这意味着,有三种类型ColorBlueGreenRed

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

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。


我不确定您是否赞成使用null数据构造函数的概念。我知道这是在Haskell中讨论常量的一种常见方法,但是不是已经多次证明它不正确吗?
kqr

@kqr:数据构造函数可以为null,但现在不再是函数。函数是带有参数并产生值的东西,即带有->签名的东西。
Frerich Raabe 2013年

值可以指向多种类型吗?还是每个值仅与一种类型相关联,仅此而已?
CMCDragonkai 2014年

1
@jrg有一些重叠,但这并不是专门因为类型构造函数,而是因为类型变量,例如ain data Color a = Red aa是任意类型的占位符。不过,您可以在普通函数中使用相同的函数,例如,类型函数(a, b) -> a采用两个值(类型a和的元组b)并产生第一个值。这是一个“通用”函数,它不决定元组元素的类型-它仅指定该函数产生的值与第一个元组元素的类型相同。
Frerich Raabe 2015年

1
+1 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.这非常有帮助。
乔纳斯(Jonas)

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


您可以在另一个数据声明的数据上下文中使用类型构造函数吗?像data Color = Blue ; data Bright = Color什么?我在ghci中尝试过,看来类型构造函数中的Color与Bright定义中的Color数据构造函数无关。只有2个Color构造函数,一个是Data,另一个是Type。
CMCDragonkai 2014年

@CMCDragonkai我认为您不能做到这一点,而且我什至不确定您要通过此实现什么。您可以使用datanewtype(例如data Bright = Bright Color)“包装”现有类型,也可以使用type来定义同义词(例如type Bright = Color)。
兰代2014年

5

第二个概念中包含“多态性”的概念。

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

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

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

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

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

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


1

类型有关:在第一种情况下,您设置类型String(公司和模型)和Int年份。在第二种情况下,您的通用性更高。abc可以是非常相同的类型,在第一示例中,完全不同的东西。例如,将年份作为字符串而不是整数可能会很有用。而且,如果您愿意,甚至可以使用您的Color类型。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.