Haskell学习笔记一:类型和类型类相关内容

内容提要:

静态类型系统;

编译时确定类型错误;

类型推导机制;

基础类型: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