内容提要:
静态类型系统;
编译时确定类型错误;
类型推导机制;
基础类型:Int,Integer,Float,Double,Bool,Char;
类型变量;
基础类型类:Eq,Ord,Show,Read,Enum,Bounded,Num,Integral,Floating;
Haskell是一门函数式编程语言,被称为最为纯粹的函数式编程语言。Haskell的类型系统非常强大,其中包含了很多有趣、抽象、某种程度上充满学术气息的特质。
Haskell属于静态类型语言,这意味着:
以上两个特征没有什么特别,很多现代编程语言,也是静态类型的,比如Java,C#等,都具备这些特征。但是Haskell的类型系统,还有一个强大特征:能够根据值或
者表达式进行类型推导,这意味着在Haskell中,绝大多数情况下,可以不显示指定值或者表达式的类型,用类似动态语言的简洁方式编写代码,同时享受静态语言的
编译时检测类型安全机制。
可以在GHCI(一个Haskell的交互式编程环境,参考https://www.haskell.org/platform/)中,通过命令“:t”后面跟值或者表达式来显示查看对应的类型,比如:
-- 两个小横线开头的行,表示是注释
:t 'a'
-- 显示结果为:'a' :: Char :t (1+1)
-- 显示结果为:(1+1) :: Num a => a
:t True
-- 显示结果为:True :: Bool
可以这样理解,'a'的类型为Char;True的类型为Bool;表达式 (1+1) 的类型为符合类型为Num类型类实例的任何类型,其中小写字母a代表类型变量,这种表示方式
是Haskell特有的,后面再详细学习介绍。
基础类型
先了解几个Haskell自带的基础类型,和其他编程语言中对应的类型很类似:
Int
表示整型数字,但是有最大值和最小值,可以通过如下的方式查询Int在机器上的最大值和最小值:
maxBound :: Int -- 我自己的机器上,结果为:9223372036854775807 minBound :: Int -- 结果为:-922337203685477808 -- maxBound 和 minBound 函数是多态的,能够运用到实现了Bounded类型类实例的任何类型,但是具体调用的时候,需要显示指明其返回值类型,比如这里的Int。
Integer
表示整型数字,但是没有限制最大值和最小值,可以表示极大或者极小的整数,但是相对于Int,效率上要低些,比如下面定义了一个阶乘函数,然后调用其计算50
的阶乘:
-- 阶乘函数定义,第一行显示指明函数的参数和返回值类型 -- 第二行是阶乘的实现,将product函数应用到有1到n组成的列表中即可 factorial :: Integer -> Integer factorial n = product [1..n] -- 在GHCI中调用factorial函数: factorial 50 -- 显示结果:30414093201713378043612608166064768844377641568960512000000000000
Float和Double
Float表示单精度的浮点数类型,Double表示双精度的浮点数类型,通过下面的一个例子来展示两者的区别:
-- 定义一个根据半径计算圆周长的函数,参数和返回值类型都是Float circumference:: Float -> Float circumference r = 2 * pi * r -- 在GHCI中执行 circumference 4.0 -- 结果为:25.132742 -- 定义一个根据半径计算周长的函数,参数和返回值类型都是Double circumference' :: Double -> Double circumference' r = 2 * pi * r -- 在GHCI中执行 circumference' 4.0 -- 结果为:25.132741228718345
Bool
Bool类型只有两个值,True和False,表示逻辑真和假,使用方式和其他语言类似:
-- 以下代码直接在GHCI中执行 :t True -- 结果为:True :: Bool :t False -- 结果为:False :: Bool (1+1) == 2 -- 结果为:True True && False -- 结果为:False True || False -- 结果为:True if True then "is true" else "is false" -- 结果为:"is true"
Char
Char表示字符类型,使用单引号包含;单字符的用途有限,字符串的使用场景更多,在Haskell中,字符串使用双引号包含,字符串只是字符列表的语法糖,如下:
-- 单字符即Char类型 :t 'C' -- 结果:'C' :: Char -- 字符串只不过是字符列表而已 :t "Seaman" -- 结果:"Seaman" :: [Char]
类型变量
为了方便说明类型变量在Haskell中的作用,先简单说明Haskell中的函数定义。因为Haskell本身是一门函数式编程语言,所以程序内容都是在定义函数,使用函数。
典型的函数定义方式有两种:
1、只有函数体,不显示说明参数和返回值类型,通过Haskell的类型推导机制确定参数和返回值类型,比如:
-- 直接定义只有函数实现的一个加法函数 sumF a b = a + b -- 在GHCI中调用 sumF 2 3 -- 结果:3
sumF 2.0 3.0
-- 结果:5.0
2、显式指定函数参数和返回值类型,比如:
-- 定义只能支持Int类型的加法函数 -- 第一行显式声明sumF'函数接受两个Int类型的参数,并且返回一个Int类型结果 sumF' :: Int -> Int -> Int sumF' a b = a + b -- 在GHCI中调用 sumF' 2 3 -- 结果:5 sumF' 2.0 3.0 -- 结果:由于函数不支持Float的参数,所以报错
很有意思的是,没有显式定义参数和返回值类型的sumF,居然自己支持多态,可以传入两个Int类型的参数,返回一个Int类型的结果;或者传入两个Float类型
的参数,返回一个Float类型的结果,可以看看Haskell为其指定的默认函数规格说明:
-- 查看sumF的函数规格说明 :t sumF -- 结果:sumF :: Num a => a -> a -> a -- sumF函数接受两个参数,返回一个结果 -- 其中两个参数和结果的类型一致 -- 它们的类型必须都是Num类型累的实例
抛开略显得古怪的函数规格说明方式(后面的学习会详细解释),我们可以看到,结果中的小写字母a就是类型变量,它代表一种抽象类型,而非具体的类型(比如Int、
Float或者Bool等等,都是具体的类型)。它表示任何Num类型类的实例(具体的类型,比如Int、Float等都是Num的实例)都可以作为参数调用该函数,并且返回值的类型
和参数是一样的。
Haskell中经常使用小写字母,比如a,f,m等代表类型变量,并且形成了一套类型体系机制,英文名称是:Algebraic Data Types,翻译过来就是:代数数据
类型。仔细体会一下,确实有很浓厚的数学意味,而且至少我能理解到有如下两个突出的优点:
1、 抽象,有些组合数据的类型操作,是无关于其中具体类型的,比如列表的一些操作(列表的长度、遍历列表等),树的一些操作(树的节点数、树的深度等),
都是和其中具体类型无关的,类似[a], Tree a都是很好的抽象;
2、 抽象,直接支持多态的函数,比如之前的加法函数,有比如Map,Reduce之类基于多个具体数据之上的高阶函数。
Haskell的类型变量很类似于C#中的泛型,并且结合Haskell特有的类型类使用,可以形成高度抽象,完全基于行为的鸭子类型,下节就具体介绍类型类。
类型类
在上面的例子中,Num就是一个类型类。和面向对象语言(比如C#,Java)中的类不同,Haskell的类型类,是关于行为的,即类型类是一种接口,其中定义了必须实现的
行为(方法)。
类型类的主要作用是定义接口行为(方法),并且对具体的类型,或者其他类型类进行约束。如果一个类型类对一个具体类型进行了约束,那么这个具体类型的定义,就
必须包括类型类的行为(方法)实现。这样的类型约束方式,或者说类型系统组成方式,都是基于行为的,是完全鸭子类型的。比如Haskell中的一个特别基础Eq类型类,
定义的行为就是关于如何比较两个类型相同的值,几乎所有Haskell的具体类型,都是Eq类型类的实例,实现了基于自己类型的相等和不等方式。
下面具体介绍在Haskell中最为基础的一些类型类:
Eq
Eq类型类用于支持相等性测试。其中定义了两个方式:==和\=,分别用于判断相等和不相等,可以在GHCI中使用:info命令,查看类型类的详细信息,如下:
:info Eq -- 显示结果如下: class Eq a where (==) :: a -> a -> Bool (\=) :: a -> a -> Bool -- 第一行是类型类的定义方式,关键字class;a是类型变量,用于抽象类型 -- where后两行,表示Eq类型类定义的两个行为(方法) -- 如果方法名全部是特殊字符组成,那么方法也称为操作符 -- 操作符使用小括号包含,天然支持中缀表达式
Haskell中的基础类型,都是实现了Eq类型类的具体类型,例如:
5 == 5 -- 结果:True 5 /= 5 -- 结果:False 'a' == 'a' -- 结果:True “Seaman” == “Seaman” -- 结果:True
同时,由于(==)和(\=)本质上都是函数,可以通过:t来参看其函数签名:
:t (==) -- 结果为: (==) :: (Eq a) => a -> a -> Bool -- 可以这样解读这个函数签名:输入两个相同类型的参数,返回一个Bool值 -- 其中输入参数是类型类Eq的具体实现类型,即a支持Eq中定义的方法
这里可以看到Haskell中类型变量表达方式的强大,它抽象了类型的表达方式,使用类似数学中代数的概念,使得概念清晰明确,极具表达性。
类型类定义起行为的时候,可以同时定义实现,比如一些和具体类型无关的实现,可以是支持该类型类的具体类型都一致的;同时如果具体类型的实现是基于自身特殊的,就
需要在定义具体类型的同时,实现其行为方法(后续学习章节详细介绍)。
Ord
Ord类型类定义了比较和顺序相关的行为(方法),具体的需要实现的方法如下:
:info Ord -- 结果如下: class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool (>=) :: a -> a -> Bool (>) :: a -> a -> Bool (<=) :: a -> a -> Bool max :: a -> a -> a min :: a -> a -> a -- 注:Ord的定义中,包括了类型限制,即必须满足Eq类型类的行为
Ord类型类支持的行为包括:compare,<,>=,>,<=,max,min,根据函数的名字和签名,可以很明确的知道其代表的含义。唯一需要说明一下的是,compare的返回值
是一个Ordering类型,这是一个具体的类型,包括了三个值:GT,LT和EQ,分别表示大于,小于和等于。一些关于实现了Ord类型类的具体类型事例如下:
"ABCD" < "EFGH" -- 结果为:True "ABCD" `compare` "EFGH" -- 结果为:LT 5 >= 2 -- 结果为:True 5 ·compare· 3 -- 结果为:GT
可以从上面的例子中看到,Haskell的基础类型,比如Int,Char,String等,都是Ord类型类的具体实现类型。
Show
Show类型类定义了如何使用字符串显示类型实例的方法,类似于C#的Object基类中的ToString()方法干的事情,比如:
-- show方法是Show类型类中定义的 show 3 -- 结果为:"3" show 5.23 -- 结果为:"5.24" show True -- 结果为:"True"
Read
Read类型类定义了如何从输入终端读入字符串,并且转型为具体类型,可以理解是和Show类型类完成相反的操作,比如:
-- read方法是Read类型类中定义的 -- 可以通过Haskell的类型推导隐式转型使用 read "True" || False -- 结果为:True read "5" + 5 -- 结果为:10 read "8.2" + 1.8 -- 结果为:10 -- 也可以进行显式地类型说明,直接读入为指定类型 read "10" :: Int -- 结果为:10
Haskell的基础类型(比如Int,Char,Float,Double等)都是Show和Read的具体实现类型。
Enum
Enum类型类定义了可以枚举的有序类型应该支持的行为,Haskell的一些基础类型,比如Bool,Char,Ordering,Int,Interger,Float,Double都是Enum类型类的
具体实现类型。Enum类型类中,最为重要的应用是Range和succ、pred方法,比如:
['a' .. 'e'] -- Range语法,两点表示从字符a到e的有序列表 -- 结果为:"abcde" [LT .. GT] -- 结果为:[LT, EQ, GT] [3 .. 5] -- 结果为:[3,4,5] succ 'B' -- 字符B的下一个字母,结果为:'C' pred 'B' -- 字符B的上一个字母,结果为:'A'
Bounded
Bounded类型类定义了支持最大值和最小值的行为,比如Int、Bool都是其具体实现类型,比如:
-- Bounded的两个方法是:minBound和maxBound minBound :: Int -- 结果为:-9223372036854775808 maxBound :: Char -- 结果为:'\1114111' maxBound :: Bool -- 结果为:True minBound :: Bool -- 结果为:False
Num、Integral和Floating
在Haskell的类型体系中,Num是关于数字的类型类,其中提供了所有数字(实数、虚数、有理数、整数、小数)都支持的行为,比如+、-、*、negate、abs等;
Integral是Num的一个具体实现,同时也是所有整数的类型类,提供了诸如rem、div、mod等基于整数的行为;
Floating是Num的另外一个具体实现,同时也是所有浮点数的类型类,提供了诸如pi、exp、sqrt、log等基于浮点数的行为。
如果需要将Integral和Floating类型的数值放在一起计算,需要显式地进行转型,一个常用的方式是:fromIntegral,这个方法的签名如下:
fromIntegral :: (Num b, Integral a) => a -> b
即把Integral类型的变量a,转型为更为一般形式的Num类型,然后就可以和其他Floating类型进行运算了。
总结:Haskell本质上是静态强类型的语言,其中静态类型特征保证了编译时能够发现任何类型方面的错误;同时类型推导机制,又十分方便编写基于问题本身的代码,而
非各种类型说明和定义。而这一切的基础,和Haskell的代数数据类型机制是密不可分的,其中的关键是类型变量和类型类。类型变量使用小写字母表示抽象而非具体的类
型,在多态语义的上下文环境中,十分具有表达力。类型类基于行为定义去约束具体的类型,从而从根本上构建出Haskell纯粹的鸭子类型。这一切结合在一起,使Haskell
的类型系统很令人着迷,我们继续学习吧!
本文部分内容和实例来自下面的网址:http://learnyouahaskell.com/types-and-typeclasses#typeclasses-101。