Haskell类型与数据构造函数

Ari*_*des 116 haskell

我正在从learnyouahaskell.com学习Haskell .我无法理解类型构造函数和数据构造函数.例如,我真的不明白这个的区别:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 
Run Code Online (Sandbox Code Playgroud)

还有这个:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  
Run Code Online (Sandbox Code Playgroud)

我知道第一个只是使用一个构造函数(Car)来构建类型的数据Car.我真的不明白第二个.

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

data Color = Blue | Green | Red
Run Code Online (Sandbox Code Playgroud)

适合所有这些?

据我所知,第三个例子(Color)是一个可以处于三种状态的类型:Blue,GreenRed.但这与我理解前两个例子的方式有冲突:这个类型Car只能处于一种状态Car,它可以构建各种参数吗?如果是这样,第二个例子如何适应?

基本上,我正在寻找一个解释,统一上述三个代码示例/结构.

kqr*_*kqr 216

data声明中,类型构造函数是等号左侧的东西.该数据构造(或多个)上等号的右边的东西签字.您可以使用需要类型的类型构造函数,并使用期望值的数据构造函数.

数据构造函数

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

data Colour = Red | Green | Blue
Run Code Online (Sandbox Code Playgroud)

在这里,我们有三个数据构造函数.Colour是一个类型,Green是一个包含值类型的构造函数Colour.类似地,Red并且Blue都是构造类型值的构造函数Colour.我们可以想象它会加油!

data Colour = RGB Int Int Int
Run Code Online (Sandbox Code Playgroud)

我们仍然只有类型Colour,但RGB不是一个值 - 它是一个函数需要三个Ints并返回一个值!RGB有类型

RGB :: Int -> Int -> Int -> Colour
Run Code Online (Sandbox Code Playgroud)

RGB是一个数据构造函数,它是一个以某些作为参数的函数,然后使用它们构造一个新值.如果您已经完成了任何面向对象的编程,那么您应该认识到这一点.在OOP中,构造函数也将一些值作为参数并返回一个新值!

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

Prelude> RGB 12 92 27
#0c5c1b
Run Code Online (Sandbox Code Playgroud)

我们通过应用数据构造函数构造了一个类型的值Colour.数据构造函数或者包含类似变量的值,或者将其他值作为其参数并创建新.如果您已经完成了以前的编程,那么这个概念对您来说应该不是很奇怪.

幕间休息

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

data SBTree = Leaf String
            | Branch String SBTree SBTree
Run Code Online (Sandbox Code Playgroud)

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

我们还看到两个数据构造函数都接受了一个String参数 - 这是它们将存储在树中的String.

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

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree
Run Code Online (Sandbox Code Playgroud)

键入构造函数

这两个SBTreeBBTree是类型构造.但是有一个明显的问题.你看他们有多相似吗?这表明你真的想要一个参数.

所以我们可以这样做:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)
Run Code Online (Sandbox Code Playgroud)

现在我们将类型变量 a作为参数引入类型构造函数.在这个宣言中,BTree已经成为一种功能.它以一个类型作为参数,并返回一个新类型.

考虑之间的区别是这里重要的具体类型(例子包括Int,[Char]Maybe Bool),这是一个可以在你的程序被分配到一个值类型和类型构造函数,你需要养活一个类型能成为分配给一个值.值永远不能是"list"类型,因为它需要是" 某事物列表".同样的精神,值永远不能是"二叉树"类型,因为它需要是"存储某些东西的二叉树".

如果我们传入,比方说,Bool作为参数BTree,它返回类型BTree Bool,这是一个存储Bools 的二叉树.替换类型变量的每次出现a与类型Bool,你可以看到自己是如何的真实.

如果您愿意,您可以查看BTree类型的功能

BTree :: * -> *
Run Code Online (Sandbox Code Playgroud)

种类有点像类型 - *表示具体类型,因此我们说BTree从具体类型到具体类型.

包起来

退一步,记下相似之处.

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

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

如果我们想要略微变化我们的值,那么带参数的数据构造函数很酷 - 我们将这些变量放在参数中,让创建值的人决定他们将要放入什么参数.同样,带参数的类型构造函数很酷如果我们想要我们的类型略有变化!我们将这些变体作为参数放置,让创建该类型的人决定他们将要放入哪些参数.

案例研究

作为家庭的延伸,我们可以考虑Maybe a类型.它的定义是

data Maybe a = Nothing
             | Just a
Run Code Online (Sandbox Code Playgroud)

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

Just :: a -> Maybe a
Run Code Online (Sandbox Code Playgroud)

换句话说,Just获取type a的值并返回type 的值Maybe a.如果我们看一下那种Maybe,我们就会看到

Maybe :: * -> *
Run Code Online (Sandbox Code Playgroud)

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

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

[] :: [Maybe]
Run Code Online (Sandbox Code Playgroud)

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

  • 声称在`data Color = Red | 绿色| Blue`"我们根本没有任何构造函数"是完全错误的.类型构造函数和数据构造函数不需要参数,参见http://www.haskell.org/haskellwiki/Constructor,它指出`data Tree a = Tip | 节点a(树a)(树a)`,"有两个数据构造函数,Tip和Node". (3认同)
  • 在您的第一个示例中,RED GREEN和BLUE都是不带参数的构造函数. (2认同)

Mat*_*hid 41

Haskell具有代数数据类型,其他语言很少.这可能让你感到困惑.

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

在Haskell中,您可以同时组合这两者.很奇怪,但是真的!

为什么称它为"代数"?好吧,书呆子谈论"总和类型"和"产品类型".例如:

data Eg1 = One Int | Two String
Run Code Online (Sandbox Code Playgroud)

Eg1值是基本上任何整数或字符串.因此,所有可能Eg1值的集合是所有可能的整数值和所有可能的字符串值的集合的"总和".因此,书呆子Eg1称为"和型".另一方面:

data Eg2 = Pair Int String
Run Code Online (Sandbox Code Playgroud)

每个Eg2包含整数和字符串.因此,所有可能Eg2值的集合是所有整数集的笛卡尔乘积和所有字符串的集合.这两组是"相乘"的,所以这是一个"产品类型".

Haskell的代数类型是产品类型的总和类型.您为构造函数提供了多个字段来生成产品类型,并且您有多个构造函数来生成(产品)总和.

作为可能有用的原因的一个示例,假设您有一些输出数据为XML或JSON的东西,它需要一个配置记录 - 但显然,XML和JSON的配置设置完全不同.所以你可能会这样做:

data Config = XML_Config {...} | JSON_Config {...}
Run Code Online (Sandbox Code Playgroud)

(显然有一些合适的领域.)你不能在普通的编程语言中做这样的事情,这就是为什么大多数人不习惯它.

  • 是的,但每当我提到"联盟"时,人们都会看着我,就像"谁曾经使用过_that_ ??" ;-) (5认同)
  • 大!只有一件事,"它们可以......几乎用任何语言构建",[维基百科](http://en.wikipedia.org/wiki/Tagged_union).:)在例如C/++中,那就是`union`s,带有标记规则.:) (4认同)

Fre*_*abe 25

从最简单的情况开始:

data Color = Blue | Green | Red
Run Code Online (Sandbox Code Playgroud)

这定义了一个Color不带参数的"类型构造函数" - 它有三个"数据构造函数" Blue,GreenRed.没有数据构造函数采用任何参数.这意味着有三种类型Color:Blue,GreenRed.

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

myFavoriteColor :: Color
myFavoriteColor = Green
Run Code Online (Sandbox Code Playgroud)

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

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

isFavoriteColor :: Color -> Bool
Run Code Online (Sandbox Code Playgroud)

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

还在我这儿?

现在,假设您不仅要创建红色/绿色/蓝色值,还要指定"强度".比如,介于0和256之间的值.您可以通过向每个数据构造函数添加一个参数来实现,因此最终得到:

data Color = Blue Int | Green Int | Red Int
Run Code Online (Sandbox Code Playgroud)

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

    myFavoriteColor :: Color
    myFavoriteColor = Green 50
Run Code Online (Sandbox Code Playgroud)

再次,它调用Green数据构造函数,我得到一个类型的值Color.

想象一下,如果你不想决定人们如何表达一种颜色的强度.有些人可能想要像我们刚才那样的数值.其他人可能没什么问题只有布尔表示"明亮"或"不那么明亮".对此的解决方案是不在Int数据构造函数中进行硬编码,而是使用类型变量:

data Color a = Blue a | Green a | Red a
Run Code Online (Sandbox Code Playgroud)

现在,我们的类型构造函数接受一个参数(我们刚刚调用的另一个类型a!),并且所有数据构造函数将采用该类型的一个参数(值!)a.所以你可以拥有

myFavoriteColor :: Color Bool
myFavoriteColor = Green False
Run Code Online (Sandbox Code Playgroud)

要么

myFavoriteColor :: Color Int
myFavoriteColor = Green 50
Run Code Online (Sandbox Code Playgroud)

注意我们如何Color使用参数(另一种类型)调用类型构造函数来获取将由数据构造函数返回的"有效"类型.这就涉及的概念,种类,你可能想在咖啡或两个杯子阅读有关.

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

  • @jrg 有一些重叠,但这并不是因为类型构造函数,而是因为类型变量,例如 `data Color a = Red a` 中的 `a`。`a` 是任意类型的占位符。但是,您可以在普通函数中使用相同的函数,例如类型为 `(a, b) -> a` 的函数采用两个值(类型为 `a` 和 `b`)的元组并产生第一个值。它是一个“通用”函数,因为它不规定元组元素的类型——它只指定函数产生与第一个元组元素相同类型的值。 (2认同)

McB*_*den 5

第二个是"多态"的概念.

a b c可以是任何类型的.例如,a可以是一个[String],b可以[Int]c[Char].

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

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

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

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

顺便说一句:确保你训练好你的大脑并对此感到舒服.这是了解Monad之后的关键.


Lan*_*dei 5

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

Maybe a = Just a | Nothing
Run Code Online (Sandbox Code Playgroud)

此类型有两个数据构造函数.Nothing有点无聊,它不包含任何有用的数据.另一方面,Just包含值a- 无论何种类型a.让我们编写一个使用这种类型的函数,例如获取Int列表的头部,如果有的话(我希望你同意这比抛出错误更有用):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None
Run Code Online (Sandbox Code Playgroud)

所以在这种情况下a是一个Int,但它也适用于任何其他类型.实际上,您可以使我们的函数适用于每种类型的列表(即使不更改实现):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x
Run Code Online (Sandbox Code Playgroud)

另一方面,您可以编写仅接受某种类型的函数Maybe,例如

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing
Run Code Online (Sandbox Code Playgroud)

总而言之,使用多态性,您可以为自己的类型提供处理不同其他类型值的灵活性.

在您的示例中,您可能在某些时候决定String不足以识别公司,但它需要具有自己的类型Company(其中包含其他数据,如国家,地址,后台帐户等).您的第一个实现Car需要更改为使用Company而不是String第一个值.你的第二个实现很好,你使用它Car Company String Int,它将像以前一样工作(当然,访问公司数据的功能需要更改).