今天我们来学习Haskell如何定义数据。在Go语言中我们有结构体来定义新的数据类型,结构体就像一个盒子把其他数据装起来达成一个包。这个"盒子"在Haskell中也是适用的。
数据定义
构造函数
Haskell定义数据的关键字是data
,比如我们定义一个Point
类型的数据。
data PointV1 = PointV1 Double Double
我们需要暂时忘记结构体的细节。data
后面的PointV1
是类型,=
后面的PointV1
是构造函数,构造函数后是参数类型。
构造函数可以和数据类型同名,也可以不同名,唯一的要求是它们都必须以大写字母开头。与之对应的另一条规则是普通函数必须以小写字母开头,这样无论是机器还是人,都可以快速区分函数和类型。
这个构造函数其实也不是必须的,没有构造函数的结果就是你没法定义该类型的变量。虽然语法上没问题,但是你却永远无法使用它。所以构造函数还是要有滴,因为构造函数决定了该类型的值如何产生。在Haskell中你不能单纯的声明某个类型的变量而在稍后赋值,因为变量是不可变的。
构造函数也可以有多个,通过|
分隔,比如Bool
的定义如下:
data Bool = True | False
Bool
类型有两个无参构造函数,你可以在GHCi中输入:t True
查看。
记录语法
在定义结构体时,我们会给每个字段命名,以便访问它们。在Haskell中我们也可以通过记录语法给构造函数的参数打上标签。这个标签相当于get函数,因为是普通函数,根据规则必须以小写字母开头。注意标签并不具备set的功能,还是那个最基本的规则,变量不可变。此外记录语法生成的函数是全局的,因此在同一命名空间下,不同数据类型的标签不能同名。我们用记录语法来定义升级版的PointV2
:
data PointV2 = PointV2 {theX :: Double, theY :: Double}
theX
相当于下面的函数,_
可以用来匹配任意值或者忽略绑定,这和Go语言中是类似的。
theX (PointV2 x _) = x
我们可以将代码加载到GHCi,然后通过theX
和theY
获取PointV2
类型值的x分量和y分量。
> p = PointV2 1 2
> theX p
> 1
> theY p
> 2
记录语法的另一个魔力是通过已有变量快速定义一个新的变量而只修改变化的部分。注意这里并不是set,而是产生了一个新的绑定。
> p = PointV2 1 2
> q = p { y = 4}
> y q
> 4
> y p
> 2
类型参数
定义数据时,也可以不指定具体类型,而是使用类型参数。比如我们可以定义一个x、y分量时任意类型的PointV3
类型。
data PointV3 a = PointV3 a a
此时我们相当于同时拥有了
PointV3 Double Double
PointV3 Float Float
PointV3 Int Int
PointV3 Char Char
PointV3 [Char] [Char]
……
等等一系列类型。
中缀构造函数
中缀构造函数必须以:
开头,此时函数名必须是符号,可以有多个,不能有字母。如果要使用记录语法,函数名必须放前面。
data Or a = a :| a
data Pair a b = (:<>) {left :: a, right :: b}
构造中缀函数和普通中缀函数在使用上也没有区别,毕竟它们都是函数。
模式匹配
模式匹配是Haskell中非常神奇的一种操作,用途广泛。这里我们需要搞清楚两个问题:
- 模式是什么
- 匹配了什么
在回答这两个问题之前我们先来实现一个计算两点之间曼哈顿距离的函数。最简单的应该是使用PointV2
类型来实现,因为我们有get函数可以用。
mhtDistV2 p1 p2 = x p1 + x p2 + y p1 + y p2
如果要计算PointV1
类型的曼哈顿距离就犯难了,我们并没有方式可以取得PointV1
的x和y分量。咋办?这题不会,模式匹配。
mhtDistV1 (PointV1 x1 y1) (PointV1 x2 y2) = x1 + x2 + y1 + y2
还记得记录语法里的theX
函数吗?其实我们早就见过模式匹配了。
模式其实指的就是构造函数,匹配也是匹配的构造函数。模式匹配会将符合模式的构造函数的参数绑定到模式变量,这样我们就能将数据中的值提取出来了。你可以理解构造函数实在装箱,模式匹配是在拆箱。
@Pattern
来思考这样一个场景,你收到一个快递,拆开包装,快递盒被你随手丢弃。此时你想退货,但是发现找不见快递盒了。如何在拆完快递之后还能保留快递盒,这就是@pattern
。
mhtDistV2' p1@(PointV2 _ _) p2@(PointV2 _ _) = x p1 + x p2 + y p1 + y p2
在let/where中使用模式匹配
mhtDistv3 p1 p2 = let PointV3 x1 y1 = p1
PointV3 x2 y2 = p2
in x1 + x2 + y1 + y2
刚接触Haskell有个非常别扭的地方就是在函数中定义变量。比如在Go语言中可以这么写:
func add(a int) int {
b := 1
return a + b
}
虽然这段代码完全没有营养,但是却很难翻译到Haskell。在Haskell的函数中,有两个地方可以绑定变量:let
和where
。我们分别用这两种方式来翻译上面的代码。
add a = let b = 1 int a + b
add' a = a + b
where b = 1
这两种方式的思维模式是相反的,let
是先绑定后使用,where
是先使用后绑定。此外let
绑定的变量只在in
中可见,而where
绑定的变量在整个函数中可见。
既然where
中可以绑定变量,那是否可以模式匹配呢?答案是可定的。
mhtDistV3' p1 p2 = x1 + x2 + y1 +y2
where PointV3 x1 y1 = p1
PointV3 x2 y2 = p2
在case中使用模式匹配
对于Bool
的定义,不知道你有没有发现一个神奇的地方,那就是True
和False
都是构造函数,其结果都是产生一个Bool
类型的值,那Haskell究竟是如何区分真假的呢?这其实也是模式匹配的威力。
bool2int a = case a of True -> 1
False -> 0
不过Haskell会建议你使用if
bool2int' a = if a then 1 else 0
if
之所以能区分True
和False
,那是因为if
只是case
的语法糖。