【问题标题】:Haskell Type vs Data ConstructorHaskell 类型与数据构造函数
【发布时间】:2013-08-14 18:52:59
【问题描述】:

我正在向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,它可以采用各种参数来构建?如果是这样,第二个示例如何适应?

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

【问题讨论】:

  • 您的 Car 示例可能有点令人困惑,因为 Car 既是类型构造函数(在 = 的左侧)又是数据构造函数(在右侧)。在第一个示例中,Car 类型构造函数不带参数,在第二个示例中它需要三个。在这两个示例中,Car 数据构造函数采用三个参数(但这些参数的类型在一种情况下是固定的,在另一种情况下是参数化的)。
  • 第一个是简单地使用一个数据构造函数 (Car :: String -> String -> Int -> Car) 来构建 Car 类型的数据。第二种是简单地使用一个数据构造函数(Car :: a -> b -> c -> Car a b c)来构建Car a b c类型的数据。

标签: haskell


【解决方案1】:

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 类型的值。如果您不熟悉二叉树的工作原理,请坚持下去。您实际上不需要知道二叉树是如何工作的,只需知道二叉树以某种方式存储Strings。

我们还看到,两个数据构造函数都采用String 参数——这是它们要存储在树中的字符串。

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

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

类型构造函数

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

所以我们可以这样做:

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

现在我们引入一个类型变量 a 作为类型构造函数的参数。在此声明中,BTree 已成为一个函数。它以 type 作为参数,并返回一个新的 type

在此考虑具体类型(示例包括Int[Char]Maybe Bool)之间的区别很重要,这是一种可以分配给您的值的类型程序和一个类型构造函数,您需要提供一个类型以便能够分配给一个值。值永远不能是“列表”类型,因为它必须是“列表某物”。本着同样的精神,值永远不能是“二叉树”类型,因为它必须是“存储某物的二叉树”。

如果我们将Bool 作为参数传递给BTree,它会返回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 接受 a 类型的值并返回 Maybe a 类型的值。如果我们查看Maybe 的类型,我们会看到

Maybe :: * -> *

换句话说,Maybe 接受一个具体类型并返回一个具体类型。

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

[] :: [Maybe]

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

【讨论】:

  • 在您的第一个示例中,RED GREEN 和 BLUE 都是不带参数的构造函数。
  • data Colour = Red | Green | Blue“我们根本没有任何构造函数”的说法是完全错误的。类型构造函数和数据构造函数不需要带参数,参见例如haskell.org/haskellwiki/Constructor 指出在data Tree a = Tip | Node a (Tree a) (Tree a) 中,“有两个数据构造函数,Tip 和 Node”。
  • @CMCDragonkai 你说的完全正确!种类是“类型的类型”。连接类型和值概念的常用方法称为依赖类型Idris 是受 Haskell 启发的依赖类型语言。使用正确的 GHC 扩展,您还可以在一定程度上接近 Haskell 中的依赖类型。 (有些人开玩笑说“Haskell 的研究是要弄清楚在没有依赖类型的情况下我们可以得到多接近依赖类型。”)
  • @CMCDragonkai 在标准的 Haskell 中实际上不可能有一个空的数据声明。但是有一个 GHC 扩展 (-XEmptyDataDecls) 可以让您这样做。正如您所说,因为没有该类型的值,所以函数 f :: Int -> Z 可能永远不会返回(因为它会返回什么?)但是它们可能对 when you want types but don't really care about values 有用。
  • 真的不可能吗?我刚刚在 GHC 中尝试过,它运行它没有错误。我不必加载任何 GHC 扩展,只需加载香草 GHC。然后我可以写:k Z,它给了我一颗星。
【解决方案2】:

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

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

【讨论】:

  • 太棒了!只有一件事,“它们可以......用几乎任何语言构建”,says Wikipedia。 :) 在例如C/++,即unions,带有标签规则。 :)
  • 是的,但每次我提到union,人们都会像“谁他妈用过那个??” ;-)
  • 在我的 C 职业生涯中,我看到很多 union。请不要让它听起来没有必要,因为事实并非如此。
【解决方案3】:

从最简单的情况开始:

data Color = Blue | Green | Red

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

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

myFavoriteColor :: Color
myFavoriteColor = Green

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

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

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。

【讨论】:

  • 我不确定我是否同意您的空数据构造函数概念。我知道这是在 Haskell 中谈论常量的常用方式,但它不是被证明不正确的好几次了吗?
  • @kqr:数据构造函数可以为空,但它不再是函数。函数是接受一个参数并产生一个值的东西,即签名中带有-> 的东西。
  • 这是迄今为止对我作为一个 Haskell 外行最有用的答案。所以人们可以用类似于 C# 或 Java 中的泛型的方式来考虑类型构造函数吗?
  • @jrg 有一些重叠,但这并不是因为类型构造函数,而是因为类型变量,例如a 中的 data Color a = Red aa 是任意类型的占位符。不过,您可以在普通功能中使用相同的功能,例如(a, b) -> a 类型的函数接受两个值的元组(ab 类型)并产生第一个值。这是一个“通用”函数,因为它不规定元组元素的类型 - 它只指定函数产生与第一个元组元素相同类型的值。
  • +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. 这很有帮助。
【解决方案4】:

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

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 Color = Blue ; data Bright = Color 这样的东西?我在ghci中试了一下,好像类型构造函数中的Color和Bright定义中的Color数据构造函数没有关系。只有 2 个 Color 构造函数,一个是 Data,另一个是 Type。
  • @CMCDragonkai 我不认为你能做到这一点,我什至不确定你想用这个实现什么。您可以使用datanewtype(例如data Bright = Bright Color)“包装”现有类型,或者您可以使用type 来定义同义词(例如type Bright = Color)。
  • 你好兰德。 Hier Stadtspatz。作为一个 Haskell 初学者,我“得到”了这个——有点——但只是因为我通过了“The Little MLer”,它直接进入了这个使用类型的奇异世界,类型构造函数作为一种递归,“脱离了身体” “糖化的 lambda 演算编程。这些东西并不容易,而且在任何地方都没有很好地解释,即突然使用发明类型作为实际表达式。如果一切都失败了,人们,休息一下,试试“The Little MLer”。尽管 ML 是一种不同的语言,但它在系统方面与 Haskell 类型几乎相同。
【解决方案5】:

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

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

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

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

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

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

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

【讨论】:

    【解决方案6】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多