Haskell 如何打造爆款数据类型

HoshinoTented

2019-07-26 11:39:03

Personal

数据类型是 Haskell 中的重要知识之一 这篇文章讲教你从一无所知到随手打造爆款数据类型 # 数据类型的定义 数据类型通常用 `data` 和 `newtype` 关键字定义 接下来,我们首先介绍 `data` ## 枚举类型 枚举类型,顾名思义,就是一个一个枚举出类型的值,与 C/C++ 中的 `enum` 类似,但功能更为强大 让我们看看枚举类型是如何定义的,比如 Haskell 标准库中的 `Bool` ```haskell DataType> :i Bool data Bool = False | True -- Defined in ‘GHC.Types’ ``` `Bool` 类型有两个值,分别的是 `False` 和 `True` 它们被 `|` 隔开,而 `|` 便是枚举类型的分隔符 ## 自定义枚举类型 我们照壶画瓢,试着定义一个属于自己的枚举类型 ```haskell data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat ``` 这看起来很不错,我们定义了一个拥有 7 个值的 Day 类型 就像在 GHCi 中输入 `True` 一样,我们试着输入 `Mon`: ```haskell DataType> Mon <interactive>:3:1: error: ? No instance for (Show Day) arising from a use of ‘print’ ? In a stmt of an interactive GHCi command: print it ``` 它报错了,为什么? 错误信息表示我们的 `Day` 类型没有实现 `Show` 类型类 > Q: 什么是类型类?什么又是 `Show`? > A: 啊。。这个以后再讲。。现在只需要知道 `Show` 类型类是用来将数据转化为字符串的 (即显示这个数据) 最简单的修复方法是使用 `deriving` 把我们自定义的数据类型修改成这样 ```haskell data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat deriving (Show) ``` 这样表示让 Haskell 使用默认实现来让我们的 Day 实现 Show 然后再在 GHCi 中输入 `Mon` ```haskell DataType> Mon Mon ``` GHCi 很成功地输出了 `Mon` ## 只有一个枚举值的枚举类型 这看起来很莫名其妙,但还是先慢慢看下去吧 枚举类型的枚举值可以只有一个,也可以和类型重名 像这样 ```haskell data People = People ``` 但它目前好像还不能被用来做什么 我们给它加点东西 ## 带有参数的枚举类型 Haskell 的枚举类型可以携带参数,就像这样 ```haskell -- Name Age data People = People String Int deriving (Show) ``` 然后可以通过这样的方法构造一个 `People` 值 ```haskell DataType> People "Hoshino" 4 People "Hoshino" 4 ``` 我们构造出了一个 People 类型的值,并把它输出了! 当然也可以有很多个带有参数的枚举类型 ```haskell data Shape = Square Int Int | Cirlce Int ``` 但这个。。 `People` 类型的构造怎么有点像函数调用呢。。 ```haskell DataType> :t People People :: String -> Int -> People ``` 天哪, `People` 居然是一个函数 ## 模式匹配 那要如何取出构造类型中的值呢 首先我们可以使用 **模式匹配** ```haskell getName :: People -> String getName (People name age) = name ``` 像构造 People 那样,把 People 解构,这样就是模式匹配 试着在 GHCi 中输入: ``` DataType> getName (People "Hoshino" 4) "Hoshino" ``` 函数很正确地取出了 `"Hoshino"` 但这样也有一个坏处,一旦构造类型内的值非常多,手写就不太现实了 于是我们可以通过修改类型的定义来做到这一点 **Record 语法** ```haskell data People = People { name :: String, age :: Int } deriving (Show) ``` 然后在 GHCi 中: ```haskell DataType> name (People "Hoshino" 4) "Hoshino" ``` 这样很棒,Haskell 自动帮我们生成了类似于 `getName` 的函数 使用 Record 语法时,可以使用另一种模式匹配的方法 ```haskell getName' :: People -> String getName' (People {name = n}) = n ``` 我们看到这里并没有匹配 `age`,但 GHCi 也没有报错,这说明使用这种写法的时候,可以选择性地匹配 你还能通过这样来构造新值 ```haskell updateName :: People -> String -> People updateName p n = p { name = n } ``` 这样的代码等价于 ```haskell updateName' :: People -> String -> People updateName' (People _ age) n = People n age ``` ## 参数化类型 你还可以给类型传递 **类型参数**,使得我们的构造类型可以存放各种类型的数据 比如 Haskell 标准库中常用的 `Maybe`: ```haskell DataType> :i Maybe data Maybe a = Nothing | Just a -- Defined in ‘GHC.Maybe ``` `Maybe` 接收了一个 a 作为类型参数,有一个 `Nothing` 值,和一个 `Just a` 值 `Maybe` 一般用于处理错误,`Nothing` 代表出错,而 `Just a` 代表成功,并包含了一个值 试着在 GHCi 中输入: ```haskell DataType> Just 1 Just 1 DataType> Nothing Nothing ``` 同样,也可以对其进行模式匹配 ```haskell isNull :: Maybe a -> Bool isNull Nothing = True isNull (Just _) = False ``` GHCi 中 ```haskell DataType> isNull (Just 1) False ``` ## 自定义参数化类型 试着自己定义一个参数化类型,比如树 ```haskell data Tree a = Leaf { value :: a } | Node { left :: Tree a, value :: a, right :: Tree a } deriving (Show) ``` ~~看起来很直观,至少比 C/C++ 强多了~~ 那接下来就可以对我们自定义的 `Tree a` 进行前中后序遍历了 ```haskell -- ... mid :: Tree a -> [a] mid (Leaf v) = [v] mid (Node left v right) = mid left ++ [v] ++ mid right -- ... ``` ## 什么是 newtype `newtype` 可以看做是 data 的简化版 它定义的数据类型只能有一个枚举值,同时也只能有一个构造值 与之等价的使用 `data` 定义的数据类型,开销会比 `newtype` 的更大 `newtype` 的语法和 `data` 相似,看起来是这样的: ```haskell -- data QAQ = QAQ Int newtype QAQ = QAQ Int ``` # 类型别名 你可以使用 `type` 关键字给类型取一个别名,就像 C 的 `typedef` 和 C++ 的 `using` ```haskell type Name = String type Age = Int -- data People = People String Int data People = People Name Age ``` 看起来更加直观了 # Haskell 中常用的数据类型 接下来,让我来介绍一下 Haskell 中常用的数据类型吧 ## Int 数字类型是最基础的类型,它的定义。。。 ```haskell DataType> :i Int data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in ‘GHC.Types’ ``` 这都什么啊? 咳咳。。重点不是这个,让我们来看看 `Int` 的范围吧 ```haskell DataType> maxBound :: Int 9223372036854775807 DataType> minBound :: Int -9223372036854775808 ``` `maxBound` 和 `minBound` 用于获取类型的最大值和最小值,需要实现 `Bounded` 类型类 不过这个以后再说,我们会发现 Haskell 的 `Int` 返回比 C 的 `int` 范围大多了 有的时候我们根本不需要这么大的范围,怎么办呢 可以导入 `Data.Int` 包,内置了 `Int8`, `Int16`, `Int32` 和 `Int64` 这四种类型 后面的数字代表占用的位数 ## Word 不!溢出了!我明明用了 `Int64`!!! 时常有这种情况,但有的时候 `Int64` 的确不够用,怎么办呢 Haskell 提供了无符号的 `Int` 类型,名为 `Word` ```haskell DataType> :m +Data.Word DataType Data.Word> minBound :: Word 0 ``` 同样,Word 也有 Word8, Word16, Word32 和 Word64 这四种类型 ## Integer 不!!!又溢出了!!!我明明用了 `Word64`!!! 如果数据过于刁钻,连 `Word64` 都装不下的时候,就只能出杀手锏了 **高精度整数** `Integer` 是 Haskell 内置的高精度整数,理论上可以存储无限精度的整数 数字字面量可以作为 `Int` 使用,也可以作为 `Integer` 使用 比如这样 ```haskell DataType> :t 1 :: Integer 1 :: Integer :: Integer ``` 但 `Integer` 和 `Int` 的互相转换通常是新手的难题 * `Integer` to `Int`: 使用 `fromInteger` 函数 ```haskell DataType> :t fromInteger fromInteger :: Num a => Integer -> a ``` 它接受一个 `Integer` 值,然后返回一个实现了 `Num` 类型类 的类型,而 `Int` 刚好实现了 `Num` 类型类 * `Int` to `Integer`: 使用 `toInteger` 函数 ```haskell DataType> :t toInteger toInteger :: Integral a => a -> Integer ``` `Int` 也实现了 `Integral` 类型类,这样传入一个 `Int` 值就能返回一个相应的 `Integer` 了 但是,`Integer` 毕竟不是原生类型,效率与原生的 `Int` 和 `Word` 还是有一定差距 不过,我再也不用手敲高精度啦哈哈哈哈哈哈哈哈哈哈哈哈嗝 ## 列表 在 GHCi 中输入: ```haskell DataType> :i [] data [] a = [] | a : [a] -- Defined in ‘GHC.Types’ ``` 看起来有点莫名其妙,这样子呢? ```haskell data [] a = [] | (:) a ([] a) ``` 如果还是不懂的话,让我们自己来实现一个列表 ```haskell data List a = Empty | Cons a (List a) deriving (Show) ``` 这样是不是比较直观了呢 `Empty` 代表一个空列表 `Cons a (List a)` 代表把一个 a 值连接在一个 List a 之前 很容易看出 Haskell 的列表其实是链表实现 关于 Haskell 的列表定义,要注意的是 `[]` 是 Haskell 内置的列表符号 所以我们自己是无法定义出名为 `[]` 的列表的 而关于 `(:)` 这个构造器名称,我们也是可以定义出类似的构造器 例如: ```haskell -- data List a = Empty | a :> (List a) deriving (Show) data List a = Empty | (:>) a (List a) deriving (Show) ``` 然后就可以 `1 :> Empty` 啦 不过要注意,`(:)` 也是 Haskell 内置的符号 ## Maybe 这是之前简略介绍过的类型,在其他语言中也有类似的数据类型,比如 Rust 中的 Option,Java8 中的 Optional 它一般被用来作为 `null` 的替代品,或者用来处理错误 ## Either 这个类型是 `Maybe` 的进阶版,它能够让错误携带错误信息,我们来看看定义: ```haskell DataType> :i Either data Either a b = Left a | Right b -- Defined in ‘Data.Either’ ``` 定义十分简单,与 `Maybe` 的差别仅仅只是多了一个类型参数而已 一般 `Right b` 被视为成功的值,因为 right 也有 “正确” 的意思 而 `Left a` 也就因此躺枪,被视为了错误 ## String 字符串也是在编程过程中经常接触的一种类型,不过它其实是 `[Char]` 的类型别名 ```haskell DataType> :i String type String = [Char] -- Defined in ‘GHC.Base’ ``` 因此也可以使用 列表 的所有函数 但事实上,这种字符串是十分低效的 Haskell 在 text 库中提供了 Data.Text.Text 类型 这是一种高效的字符串,不过因为篇幅原因,在这里不过多介绍 如果有兴趣,可以在[这里](http://hackage.haskell.org/package/text)了解更多关于 Text 类型的内容 ## 函数 是的,函数也是一种数据类型 ```haskell DataType> :i (->) data (->) (a :: TYPE q) (b :: TYPE r) -- Defined in ‘GHC.Prim’ infixr 0 -> ``` `infixr` 是什么? 这是 Haskell 对运算符优先级的声明语句 `infixr` 代表运算符是右结合的,`infixl` 则代表左结合 而 0 代表了运算符的优先级,0 为最低,9 则是最高 因为 `(->)` 是右结合的,所以也能够说明为什么 `Int -> Int -> Int` 和 `Int -> (Int -> Int)` 等价了 # 奇怪的共同点 在刚才介绍的这些数据类型中,很容易找到一个共同点 构造器和类型的名称都不是非小写字母的 这是因为 Haskell 会把大写开头识别为类型,而小写开头识别为函数