Haskell 入门笔记(四)

类型系统

强大的类型系统是 Haskell的 一个非常大的优势。

Haskell 所有表达式类型在编译时判断。这样的话,可以使得代码更加安全,比如说,拿一个整数和一个字符串进行除法运算是没办法进行的,那么在编译器就会直接报错,不会等到运行时程序崩溃才知道。Haskell 与 Java 不一样,Haskell 能够进行类型推断(Type Inference),也就是说,你不需要明确的说 100 是个数字,或者说是整型,编译时能推断出这是一个整型。

在 GHCi 中,我们可以使用 :t 命令来检测一个表达式的类型。

Prelude> :t 'q' 
'q' :: Char

Prelude> :t "aaa" 
"aaa" :: [Char]

:: 操作符的含义是「具有 … 类型」。也就是说,根据上面的结果,我们知道,字符 q 的类型是 「Char」。一般来说,Haskell 的类型的首字母都是大写,比如上面提到的 Char,还有 Bool 或者 Boolean。[] 代表 List,[Char] 代表元素类型为 Char 的 List。() 则代表 Tuple,('a','a')的类型是 (Char,Char)

显式类型声明

除了表达式之外,函数也是有类型的。我们在定义函数的时候,可以显式地给函数声明其类型。我们在前面讲过一个去除字符串中大写字母的 List Comprehension:

removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]

对于这样一个函数,很明显,其输入和输出都是字符串,也就是字符的 List,因此,我们可以这样声明函数的类型:

removeNonUppercase :: [Char]->[Char]a

上面这个声明的含义是,函数 removeNonUppercase 接收一个[Char] 类型的参数(例如字符串),并且返回一个 [Char](例如字符串)。那怎么去指定一个接收多个参数的函数的类型呢?比如说有一个函数叫 addThree,接收三个参数,将这三个参数的值相加并且返回。我们可以这样指定 addThree 的函数类型

addThree :: Int->Int->Int->Int

也就是说,最后一个会被当做返回值来解析,前面的都会被当做参数来解析。如果说你不知道你要写的函数到底应该是什么类型,你可以先把函数写出来,然后使用 :t 命令看看到底是什么类型,最后再补上函数类型。

常见的Haskell类型

类型 说明
Int 整型,但是能表示的整数有界限(达到一定程度就会溢出),效率更高
Integer 整型,能够表示的整数没有界限,效率低
Float 单精度浮点数
Double 双精度浮点数
Bool 布尔值,只有 True 和 False 两个值
Char 单个Unicode字符
Tuple 具体的 Tuple 类型取决于元素的类型和个数,理论上有无数 Tuple 类型,但是实际上Tuple最多只能有63个元素

类型变量(Type Variable)

有时候函数需要能够处理多种类型的数据,我们以 head 函数为例。首先看看 head 函数的类型:

Prelude> :t head 
head :: [a] –> a

我们可以看到,函数 head 接收一个 List 作为输入,返回 List 中的一个元素。但是这个元素到底是 Char 还是 Int 还是 Bool 并不重要。这个 a 是什么?我们说过所有的类型都是以大写字母打头的,a 显然不是一种我们所不知道的类型。a 实际上就是我们这里说的类型变量的一个例子。类型变量能够允许函数以一种安全的方式操作多种类型,这一点类似于 Java 中的泛型。使用类型变量的函数在 Haskell 中称为多态函数(Polymorphyc function)。head 函数的定义的含义是:head 接收一个装有任何元素的 List,返回这种类型一个值。英语中单词 a 也表示泛指, a pen, a apple 等等。

我们再看看 fst 函数的类型定义:

Prelude> :t fst 
fst :: (a, b) –> a

这个函数接收一个 pair,然后返回第一个元素,至于这个 pair 的元素可以是任何类型,这里的a,b都是类型变量。需要说明的是,这里的 a 和 b 虽然都是类型变量,但是不意味着他们一定是不同的类型。a,b 这种类型变量就像占位符变量一样,表示这个地方有一个某某类型的变量。

Type Class

Type Class 我也不知道该怎么翻译比较合适。Type Class 实际上是一种接口,它定义一些行为,当某个变量是这个 Type Class 的实例时,那么它可以实现这个 Type Class 所描述的行为。Type Class 一般指定一组函数,一个变量是该 Type Class 的实例,我们就需要确定这些函数对于这个变量本身有什么意义(也就是说这个变量要有自己的实现)。

定义相等性的 Type Class 就是一个很好的例子。很多类型都可以用 == 来看值是否相等。我们先看看 == 运算符的函数签名:

Prelude> :t (==) 
(==) :: Eq a => a -> a –> Bool

实际上 == 是一个函数,基本上 +-* 以及几乎所有的运算符都是函数。这里出现了一个新的符号 =>,所有出现在这个符号之前的部分叫做 class constraint(类的约束)。这个函数类型的意思是:== 函数接收两个值,他们同样属于类型 Eq,函数最终返回一个 Bool 值。

Eq 就属于 Type Class,它提供了判断值是否相等的接口。而这些值必须是相同类型才有比较的意义,这些值可以是 Eq 的实例。事实上,在标准的 Haskell 中,几乎所有类型都是 Eq 的实例。需要特别指出的是,Type Class 并不是面向对象编程语言中的 Class。下面我们一起看看 Haskell 中常见的几种 Type Class:

  • Eq

Eq 用来提供检测值是否相等的接口。它的两个实现是 ==/=。这意味着如果在一个函数的定义中出现了 Eq class constraint,那么这个函数的定义中肯定用到了 == 或者是 /=。如果一种类型实现一个函数,他就要定义使用这个类型的值时,该函数到底做些什么。我们看几个 Eq 实例进行相等性比较时的例子:

Prelude> 5 == 5 
True 
Prelude> 'q' == 'q' 
True 
Prelude> "Hello"=="hello" 
False 
Prelude> "Hello"=="Hello" 
True 
Prelude> pi == 3.14 
False

我们可以看到,字符串的比较规则是遵循 List 的相等性比较,与 Java 中的比较引用是不一样的。

  • Ord

Ord 是一种为那些可以将值放在某种顺序排列中的类型设计的 Type Class。我们看看 > 函数的类型:

Prelude> :t (>) 
(>) :: Ord a => a -> a –> Bool

>== 比较类似,都接收两个参数,然后返回一个 Bool 值。Ord Type Class 涉及到了所有的比较函数:><>=<=

compare 函数接收两个参数,这两个参数的类型都是 Ord 的实例,然后返回一个 Ordering。Ordering 是一个值可以是 GT、LT 或者 EQ 的类型,分别代表大于、小于和等于。我们看几个例子:

Prelude> "abcd" `compare` "bbcd" 
LT 
Prelude> "abcd" `compare` "abbd" 
GT 
Prelude> "abcd" `compare` "abcd" 
EQ
  • Show

类型是 Show 这个 Type Class 的实例的值可以被显示为字符串。对于所有属于 Show 这个 Type Class 的实例的类型来说,使用最多的函数式 show(s小写)。我们看几个例子:

Prelude> show 3 
"3" 
Prelude> show True 
"True"
  • Read

Read 可以看做是 Show 的反面。read 函数接收一个字符串,然后返回一个类型是 Read 的实例的值。看例子:

Prelude> read "True" || False 
True

Prelude> read "5"-2 
3

Prelude> read "[1,2,3,4]" ++ [5] 
[1,2,3,4,5]

目前为止都一切正常,我们再看一个例子:

Prelude> read "5"

<interactive>:30:1: 
    Ambiguous type variable `a0' in the constraint: 
      (Read a0) arising from a use of `read' 
    Probable fix: add a type signature that fixes these type variable(s) 
    In the expression: read "5" 
    In an equation for `it': it = read "5"

当我们直接 read "5" 时,GHCi 不知道该返回什么。我们之前的例子都将 read 返回的结果再参与某种运算,这样 GHCi 才好进行类型推断,这就是为什么 read "5" 没办法返回值的原因。我们看一下read 函数的类型:

Prelude> :t read 
read :: Read a => String –> a

我们看到,read 函数接收 String,但是返回一个类型是 Read 的实例的值。但是类型是 Read 实例的类型太多了,GHCi 不知道到底选哪一种类型。这种情况下,我们可以使用类型注解(type annotation)。我们看例子是最直接的:

Prelude> read "5" :: Int 
5 
Prelude> read "5" :: Float 
5.0

对于 read 来说还需要举一个例子:

Prelude> [read "True",False,True,False] 
[True,False,True,False]

因为 List 中的每一个元素必须属于同种类型,所以 read "True" 的返回值必须和其他元素类型一样,也就是 Bool,这样,GHCi 就知道该怎么返回值了。

  • Enum

Enum 的实例是那种值有序的类型——他们的值可以被枚举。Enum Type Class 最大的优势是可以在 Ranges 中使用其值。他们还定义了successors 和 predecessors, 我们可以分别通过 succ 和 pred 两个函数获得。Bool、Char、Ordering、Int、Integer、Float、Double 是这个 Type Class 的实例,我们看例子:

Prelude> ['a'..'e'] 
"abcde"

Prelude> [LT .. GT] 
[LT,EQ,GT] 
Prelude> [3 .. 5] 
[3,4,5] 
Prelude> succ 'B' 
'C' 
Prelude> pred 'B' 
'A'
  • Bounded

那些是 Bounded 实例的类型有一个上限值和一个下限值。分别可以使用 minBound 和 maxBound 查看:

Prelude> minBound::Int 
-2147483648 
Prelude> maxBound::Int 
2147483647

minBound 和 maxBound 的类型都是 Bounded a=>a。准确来说,他们是多态常量。Tuple 中所有元素类型都是 Bounded 的话,那么这个 Tuple 也被认为是 Bounded 的实例。

  • Num

Num 是数字 Type Class,它的实例都是数字。所有的数字都是多态常量。也就是说我们可以将它制定成 Num 下属类型中的任何一种:

Prelude> 6::Int 
6 
Prelude> 6::Float 
6.0

要成为 Num Type Class 的实例,这个类型必须要已经是 Eq 和 Show Type Class 的实例。

  • Floating

顾名思义,这种 Type Class 的实例类型就是用来存储浮点数的,就两种类型 Float 和 Double。

  • Integral

包括 Int 和 Integer 两种。介绍两个函数 fromIntegral 和 length,先看看两个函数的签名,再看看怎么使用:

Prelude> :t fromIntegral 
fromIntegral :: (Integral a, Num b) => a -> b 
Prelude> :t length 
length :: [a] –> Int

Prelude> fromIntegral (length [1,2,3,4]) + 3.4 
7.4

Tips

Type Class 实际上是一个抽象的接口,所以一个类型可以是多种 Type Class 的实例,同样,一种 Type Class 有很多实例;
有时候一种类型必须先是一种 Type Class 的实例才会被允许成为另一个Type Class 的实例。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容