ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

d
declension

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 类型的值。数据构造函数要么像变量一样包含一个值,要么将其他值作为其参数并创建一个新的。如果你以前做过编程,这个概念对你来说应该不会很陌生。

中场休息

如果您想构建一棵二叉树来存储 String,您可以想象做类似的事情

data SBTree = Leaf String
            | Branch String SBTree SBTree

我们在这里看到的是一个类型 SBTree,它包含两个数据构造函数。换句话说,有两个函数(即 LeafBranch)将构造 SBTree 类型的值。如果您不熟悉二叉树的工作原理,请坚持下去。您实际上不需要知道二叉树是如何工作的,只需知道二叉树以某种方式存储 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 已成为一个函数。它接受一个 type 作为其参数,并返回一个新的 type

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

如果我们将 Bool 作为参数传递给 BTree,它会返回类型 BTree Bool,它是一个存储 Bool 的二叉树。将每次出现的类型变量 a 替换为类型 Bool,您就可以亲眼看到它的真实性。

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

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

[] :: [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,它给了我一颗星。
K
Kapol

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

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

在 Haskell 中,您可以同时将这两者结合起来。奇怪,但真实!

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

data Eg1 = One Int | Two String

Eg1 值基本上是要么 整数或字符串。所以所有可能的 Eg1 值的集合是所有可能的整数值和所有可能的字符串值的集合的“总和”。因此,书呆子将 Eg1 称为“求和类型”。另一方面:

data Eg2 = Pair Int String

每个 Eg2 值都由 both 一个整数和一个字符串组成。所以所有可能的 Eg2 值的集合是所有整数的集合和所有字符串的集合的笛卡尔积。这两个集合“相乘”在一起,所以这是一个“产品类型”。

Haskell 的代数类型是乘积类型的总和类型。您为构造函数提供多个字段来创建产品类型,并且您有多个构造函数来计算(产品的)总和。

举例说明这可能有用的原因,假设您有一些以 XML 或 JSON 格式输出数据的东西,并且它需要一个配置记录 - 但显然,XML 和 JSON 的配置设置完全不同。所以你可能会做这样的事情:

data Config = XML_Config {...} | JSON_Config {...}

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


伟大的!只有一件事,“它们可以......用几乎任何语言构建”,says Wikipedia。 :) 在例如 C/++ 中,这是 unions,带有标签规则。 :)
是的,但每次我提到 union 时,人们都会以“谁他妈用过那个??”的表情看着我。 ;-)
在我的 C 职业生涯中,我看到了很多 union。请不要让它听起来没有必要,因为事实并非如此。
F
Frerich Raabe

从最简单的情况开始:

data Color = Blue | Green | Red

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

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

myFavoriteColor :: Color
myFavoriteColor = Green

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

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

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 有一些重叠,但这并不是因为类型构造函数,而是因为类型变量,例如 data Color a = Red a 中的 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. 这很有帮助。
L
Landei

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

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

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

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

第一个类型是固定的:company 是 String,model 是 String,year 是 Int

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

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

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

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


M
Matthias

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅