跳转到内容

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}结构视为一个设置器方法,该方法修改了预先存在的yx的值。它不是那样的——请记住,在 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)

这将变量lhConfiguration中的localHost字段匹配,并将变量rhremoteHost字段匹配。这些匹配当然会成功。您也可以通过在这些位置放置值而不是变量名称来约束匹配,就像您对标准数据类型所做的那样。

如果您使用的是 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 对应的记录;否则,它将是NothingMaybe 是在 Haskell 中表示失败的最简单和最常见的方式。它有时也出现在函数参数的类型中,作为使它们可选的一种方式(目的是传递Nothing 等同于省略参数)。

您可以以完全相同的方式参数化typenewtype 声明。此外,您可以以任意方式组合参数化类型来构造新类型。

多个类型参数

[编辑 | 编辑源代码]

我们也可以有多个类型参数。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 参数化类型的灵活性会导致类型声明中的错误,这些错误类似于类型错误,只是它们发生在类型声明中而不是在程序本身中。“类型”中的错误被称为“类型”错误。您不会使用类型编程:编译器会自行推断它们。但是,如果您错误地参数化了类型,则编译器将报告类型错误。

华夏公益教科书