概览
类型系统是Haskell的重要组成部分,和函数有着紧密的联系。在相关入门书籍中一般放在和函数一起介绍。
Haskell中的类型系统有三个特点。
- 强类型:如Javascript中的强制类型转换在Haskell中是完全不被接受的
- 静态类型:编译器在编译期就知道每一个值和表达式的类型。
- 类型推导:在绝大多数情况下Haskell编译器都能推导出表达式的类型
相关的概念很多,比如Type, TypeClass,Kind 等,文本讲简要介绍一下Type TypeClass 相关的内容。
类型 Type
在GHCI中通过:t命令我们可以查看任何表达式的类型
Prelude>:t 'a'
'a' :: Char
Prelude>:t True
True :: Bool
Prelude>:t "Hello"
"Hello"::[Char]
Prelude>:t 1
1::Num p => p
Prelude>:t (==)
(==) :: Eq a => a -> a -> Bool
这里面有一些很常见的东西,比如:
True是Bool类型的,同样属于Bool类型的还有False'a'是Char类型的
常见的类型还有
Int有界有符号整数,数字范围和操作系统于GHC的位数有关,对于32位的操作系统和GHC,其范围是-2^31到2^31-1。Integer类似于Java中的BigInteger,计算效率效率比Int低,但是可以用来标识任意范围的整数Float单精度浮点数Double双精度浮点数
除了这些比较常见的东西,大家对(==) :: Eq a => a -> a -> Bool这段儿含义,一定是一头雾水。这是一个类型声明,让我们拆开一点点理解:
::符号用于声明其左侧表达式的类型,可以读作『类型为』。->符号用于在声明函数类型时,分隔参数。比如一个函数的类型是Int->Int->Bool,那么意味着这个函数接受两个Int类型的变量,返回一个Bool类型的值。=>符号表示『类型约束』,Eq a => a意味着a属于Eq这个TypeClass。
这样(==) :: Eq a => a -> a -> Bool这个类型声明的含义就是对于(==)这个函数,需要两个参数a,a从属于Eq这个TypeClass,返回一个Bool类型的值。
读到这里我想大家很自然会有两个疑问:
TypeClass是什么呢?- 为什么需要
TypeClass
对于第两个问题,我们可以先这样理解。不同类型的数据可能会有相同的行为。比如Int和Float都可以进行加减乘除运算。比如Char和Bool都通过==来比较是否相等。
为了描述不同类型数据之间一致的行为,TypeClass的概念也就产生了。TypeClass是对不同类型中一致行为的抽象。一个类型如果属于某个TypeClass那么该类型必然实现了该TypeClass所描述的行为(通常意味着一些函数)。
类型类TypeClass
前面关于TypeClass的描述或许还是有些抽象,为了能够对TypeClass有个更清晰的描述,让我们通过GHCI内置的:i命令来入手。
相等类型类Eq
输入:i Eq我们会得到一长串关于类型类Eq的信息,我们截取其中中一部分来看。
在Eq的定义中描述了其成员所要实现的两个函数,这两个函数都是接受两个参数得到一个Bool值。在后续展示的信息中,我们可以看到Bool,Char ,Double,Int等类型都是Eq的instance,因而对于这些类型的数据我们可以进行 (==),(/=)运算。
Prelude> (==) 1 1
True
Prelude> (==) 'a' 'a'
True
Prelude> (==) True False
False
Prelude> (/=) 1 1
False
Prelude> (/=) 'a' 'a'
False
Prelude> (/=) True False
True
使用中缀风格的函数也是可以的
Prelude> 1 == 1
True
Prelude> 1 /= 1
False
有序类型类Ord
如果说Eq类型类中,描述了其成员类型可以做相等,不等判断的行为,那么Ord类型类的成员则可以做(<), (>), (<=), (>=) 之类用于比较大小的操作。
Prelude> (>) 1 0
True
Prelude> (<) 1 0
False
Prelude> (>) 'a' 'z'
False
枚举类型类Enum
Enum是一个非常有趣的类型类.其成员类型都是连续可枚举的。每个值都会有有后继子(successer)和前置子(predecesor),分别可以通过succ和pred函数来获取。比如1的前置子是0后继子是2,2的前置子是1后继子是3。依次类推,对于任意值,我们可以推导出其后续n个值分别是什么。看到这里,就不得不提起曾经在《慢慢学Haskell(一):Hello Haskell》中介绍过的的Range。通过[2..]表示大于2的正整数列表,进而进行素数的遍历。Enum的成员类型有Bool, Char, Int, Integer等,都可以用Range风格的写法:
Prelude> succ False
True
Prelude> pred True
False
Prelude> ['a'..'x']
"abcdefghijklmnopqrstuvwx"
Prelude> [1..8]
[1,2,3,4,5,6,7,8]
有界类型类Bounded
Bounded类型类,描述有上线边界的数据类型。比如前面提到的Int类型,对于32位的操作系统和GHC,其下界是-2^31,上界是2^31-1。通过minBound和maxBound函数可以获取某个类型的边界。
Prelude> minBound :: Int
-2147483648
Prelude> maxBound :: Char
'\1114111'
Prelude> maxBound :: Bool
True
Prelude> minBound :: Bool
False
数字类型类Num
数字类型类中描述了数字的通用行为,比如加减乘+,-,*,取绝对值abs等函数。
Prelude> abs (-1.2)
1.2
Prelude> 1.2 * 1.3
1.56
其中有一个比较有趣的函数fromInteger。因为Haskell强类型的缘故,1.2::Float和2::Integer 是没有办法直接做运算的,但是可以通过fromInteger将Integer类型的数据转换为更为通用的Num a => a类型,再进行运算。
Prelude> num1 = 2::Integer
Prelude> num2 = 1.2::Float
Prelude> num2 + num1
<interactive>:137:8: error:
• Couldn't match expected type ‘Float’ with actual type ‘Integer’
• In the second argument of ‘(+)’, namely ‘num1’
In the expression: num2 + num1
In an equation for ‘it’: it = num2 + num1
Prelude> num2 + (fromInteger num1)
3.2
类似的函数还有fromIntegral。
类型系统的好处
关于类型系统的好处,网络上总结的很多,比如:
-
提高程序的性能
-
检测错误,提高代码健壮性
-
降低重构的难度和风险
-
...
对于Haskell还有一个好处就是可以更方便的检索文档。Haskell的Hoogle引擎,可以让我们方便的通过函数类型来搜索相关函数。
比如我们想搜索一个函数接收一个Float类型参数,得到一个Int类型的值,我们可以通过Float -> Int来进行搜索。
这种搜索API的方式,提供了便捷,也在某种程度上塑造了编程者的思维。
总结一下
在计算机看来,程序所操作的数据,无非是一串无意义的二进制数。但是对于人类来说,把数据划分成不同的类型具有更好的可读性和可操作性。对于高级程序语言来说类型的存在可以让数据更便于处理。
不同类型的数据,有时候有着相似的行为。比如Int和Char类型的数据,虽然类型不同。但都可以进行枚举和做相等判断的。对于这类相似的行为抽象的需求,催生了TypeClass。如果要与面向对象编程做类比的话,Type和TypeClass的关系,类似于类和接口。
至此本文的内容已接近尾声,Haskell的类型系统复杂而强大。笔者在学习的时候也是磕磕绊绊学了好久才逐渐理解一些皮毛。后面还会再通过一道两篇文章来纪录自己的学习所得。