Haskell/数据类型进阶
data
声明的一种特殊情况是枚举类型——一种数据类型,其中所有构造函数都没有参数。
data Month = January | February | March | April | May | June | July
| August | September | October | November | December
您可以混合带有参数和不带参数的构造函数,但结果不再被称为枚举类型。以下示例不是枚举类型,因为最后一个构造函数需要三个参数。
data Colour = Black | Red | Green | Blue | Cyan
| Yellow | Magenta | White | RGB Int Int Int
正如您将在后面讨论类和派生时看到的那样,在实践中区分什么是枚举类型和什么不是枚举类型是有原因的。
顺便说一下,Bool
数据类型是枚举类型。
data Bool = False | True
deriving (Bounded, Enum, Eq, Ord, Read, Show)
考虑一种数据类型,其目的是保存配置设置。通常,当您从这种类型中提取成员时,您实际上只关心众多设置中的一两个。此外,如果许多设置具有相同的类型,您可能会经常发现自己想知道“等等,这是第四个还是第五个元素?” 一种澄清方法是编写访问器函数。考虑以下为终端程序制作的配置类型。
data Configuration = Configuration
String -- User name
String -- Local host
String -- Remote host
Bool -- Is guest?
Bool -- Is superuser?
String -- Current directory
String -- Home directory
Integer -- Time connected
deriving (Eq, Show)
然后您可以编写访问器函数,例如
getUserName (Configuration un _ _ _ _ _ _ _) = un
getLocalHost (Configuration _ lh _ _ _ _ _ _) = lh
getRemoteHost (Configuration _ _ rh _ _ _ _ _) = rh
getIsGuest (Configuration _ _ _ ig _ _ _ _) = ig
-- And so on...
您还可以编写更新函数来更新单个元素。当然,如果您在配置中添加或删除元素,所有这些函数现在都必须采用不同数量的参数。这非常烦人,并且很容易出现错误。谢天谢地,有一个解决方案:我们只需在数据类型声明中为字段命名,如下所示。
data Configuration = Configuration
{ username :: String
, localHost :: String
, remoteHost :: String
, isGuest :: Bool
, isSuperuser :: Bool
, currentDir :: String
, homeDir :: String
, timeConnected :: Integer
}
这将自动为我们生成以下访问器函数。
username :: Configuration -> String
localHost :: Configuration -> String
-- etc.
这也为我们提供了一种方便的更新方法。以下是一个“工作目录后”和“更改目录”函数的简短示例,这些函数适用于Configuration
。
changeDir :: Configuration -> String -> Configuration
changeDir cfg newDir =
if directoryExists newDir -- make sure the directory exists
then cfg { currentDir = newDir }
else error "Directory does not exist"
postWorkingDir :: Configuration -> String
postWorkingDir cfg = currentDir cfg
因此,一般来说,要将值y
中的字段x
更新为z
,您需要编写y { x = z }
。您可以更改多个字段;每个字段之间用逗号隔开,例如,y {x = z, a = b, c = d }
。
注意
熟悉面向对象语言的人可能会在听了所有关于“访问器函数”和“更新方法”的讨论后,将y{x=z}
结构视为一个设置器方法,该方法修改了预先存在的y
中x
的值。它不是那样的——请记住,在 Haskell 中变量是不可变的。因此,使用上面的示例,如果您执行类似conf2 = changeDir conf1 "/opt/foo/bar"
的操作,conf2
将被定义为一个Configuration
,它与conf1
完全相同,除了currentDir
为"/opt/foo/bar"
,但conf1
将保持不变。
当然,您可以继续像以前一样对Configuration
进行模式匹配。命名字段只是语法糖;您仍然可以编写类似以下内容的东西。
getUserName (Configuration un _ _ _ _ _ _ _) = un
但没有必要这样做。
最后,您可以对命名字段进行模式匹配,如下所示。
getHostData (Configuration { localHost = lh, remoteHost = rh }) = (lh, rh)
这将变量lh
与Configuration
中的localHost
字段匹配,并将变量rh
与remoteHost
字段匹配。这些匹配当然会成功。您也可以通过在这些位置放置值而不是变量名称来约束匹配,就像您对标准数据类型所做的那样。
如果您使用的是 GHC,那么使用语言扩展NamedFieldPuns
,还可以使用以下形式。
getHostData (Configuration { localHost, remoteHost }) = (localHost, remoteHost)
它可以与普通形式混合使用,如下所示。
getHostData (Configuration { localHost, remoteHost = rh }) = (localHost, rh)
(要使用此语言扩展,请在解释器中输入:set -XNamedFieldPuns
,或在源文件开头使用{-# LANGUAGE NamedFieldPuns #-}
编译指示,或将-XNamedFieldPuns
命令行标志传递给编译器。)
您可以使用以下所示的旧方法创建Configuration
的值,或者使用命名字段的类型创建,如第二个定义所示。
initCFG = Configuration "nobody" "nowhere" "nowhere" False False "/" "/" 0
initCFG' = Configuration
{ username = "nobody"
, localHost = "nowhere"
, remoteHost = "nowhere"
, isGuest = False
, isSuperuser = False
, currentDir = "/"
, homeDir = "/"
, timeConnected = 0
}
第一种方式要短得多,但第二种方式要清晰得多。
警告:第二种风格将允许您编写省略字段但仍然可以编译的代码,例如。
cfgFoo = Configuration { username = "Foo" }
cfgBar = Configuration { localHost = "Bar", remoteHost = "Baz" }
cfgUndef = Configuration {}
尝试评估未指定的字段将导致运行时错误!
参数化类型类似于其他语言中的“泛型”或“模板”类型。参数化类型采用一个或多个类型参数。例如,标准 Prelude 类型Maybe
定义如下。
data Maybe a = Nothing | Just a
这意味着类型Maybe
采用类型参数a
。您可以使用它来声明,例如。
lookupBirthday :: [Anniversary] -> String -> Maybe Anniversary
lookupBirthday
函数接受一个生日记录列表和一个字符串,并返回一个Maybe Anniversary
。对这种类型的通常解释是,如果通过字符串给出的名称在周年纪念列表中找到,则结果将是Just
对应的记录;否则,它将是Nothing
。Maybe
是在 Haskell 中表示失败的最简单和最常见的方式。它有时也出现在函数参数的类型中,作为使它们可选的一种方式(目的是传递Nothing
等同于省略参数)。
您可以以完全相同的方式参数化type
和newtype
声明。此外,您可以以任意方式组合参数化类型来构造新类型。
我们也可以有多个类型参数。Either
类型的示例如下。
data Either a b = Left a | Right b
例如。
pairOff :: Int -> Either String Int
pairOff people
| people < 0 = Left "Can't pair off negative number of people."
| people > 30 = Left "Too many people for this activity."
| even people = Right (people `div` 2)
| otherwise = Left "Can't pair off an odd number of people."
groupPeople :: Int -> String
groupPeople people =
case pairOff people of
Right groups -> "We have " ++ show groups ++ " group(s)."
Left problem -> "Problem! " ++ problem
在这个例子中,pairOff
指示了如果您将一定数量的人分成小组进行活动,您将拥有多少个小组。它还可以让您知道是否人员过多或是否有人会被排除在外。因此,pairOff
将返回一个表示您将拥有的组数的 Int,或者一个描述您无法创建组的原因的 String。
Haskell 参数化类型的灵活性会导致类型声明中的错误,这些错误类似于类型错误,只是它们发生在类型声明中而不是在程序本身中。“类型”中的错误被称为“类型”错误。您不会使用类型编程:编译器会自行推断它们。但是,如果您错误地参数化了类型,则编译器将报告类型错误。