Real World Haskell 读书笔记(2)类型与函数
为什么要关心类型? p17
在Haskell中,所有的表达式与函数都有类型。
类型给予一组字节以意义,告诉程序这些字节应该以什么方式组织起来。同时类型也提供了一层抽象,让我们不用关心它底层的细节,而只用关心它是个Int还是个String或是什么。当然类型系统除了抽象之外,还提供类型检查等功能。
Haskell的类型系统 p18
Haskell的类型系统是strong,static的,并且可以automatically inferred(自动推导)的。也就是说Haskell是门强类型静态语言。
一个语言的类型为强类型,表示该语言的类型系统不会容忍任何类型错误,并且不允许有隐式转换。这里说的强弱都是指类型系统的*度,例如C允许一定的隐式转换,而Haskell完全不允许隐式转换,那么我们可以说Haskell的类型要更强。例如:
ghci> 1 && True
这个语句在Haskell中是无法通过编译的,因为(&&)要求必须接受两个类型为Bool的参数。
静态是指编译器在编译期知道每个值和表达式的类型。如果类型不匹配,编译期或者解释器就会给出错误信息,而Haskell的类型又是如此之强,以至于只要我们的程序能通过编译,在运行期是不可能发生类型错误的。
自动推导
Haskell编译器在绝大部分时间内可以自动推导出表达式的类型,我们也可以显式地指定每个表达式的类型,但这通常不是必须的。
基本类型 p21
首先,Haskell中的类型都是以大写字母开头的。
Char
单个 Unicode 字符。
Bool
表示一个布尔逻辑值。这个类型只有两个值: True 和 False 。
Int
带符号的定长整数。这个值的准确范围由机器决定:在 32 位机器里, Int 为 32 位宽,在 64 位机器里, Int 为 64 位宽。Haskell 保证 Int 的宽度不少于 28 位。(数值类型还可以是 8位、16 位,等等,也可以是带符号和无符号的,以后会介绍。)
Integer
不限长度的带符号整数。 不如 Int 常用,因为需要更多的内存和更大的计算量。但是,对 Integer 的计算不会造成溢出,因此使用 Integer 的计算结果更可靠。
Double
用于表示浮点数。长度由机器决定,通常是 64 位。(Haskell 也有 Float 类型,但是并不推荐使用,因为编译器都是针对 Double 来进行优化的,而Float 类型值的计算要慢得多。)
函数 p22
在Haskell中,调用一个函数,只需在函数名之后写出参数即可,而无需用圆括号括住参数,如:
ghci> odd 3
True
ghci> compare 2 3
LT
函数调用比其他的操作符具有更高的优先级,也就是说,
(compare 2 3) == LT 等价于compare 2 3 == LT
类型签名 p22
::操作符与其之后的类型就是类型签名。
虽然Haskell大多时候能自动推导出正确的类型,但是我们也可以明确的告诉编译器一个值或是函数是什么类型的,如:
ghci> :type 'a' --自动推导
'a' :: Char
ghci> 'a' :: Char –显式签名
'a'
当然,类型签名必须正确,否则 Haskell 编译器就会报错。例如试图将一个Char指定为Int,
ghci> 'a' :: Int是会报错的
复合数据类型 p23
两种常用的复合数据类型,
Lists
用一对方括号表示。可以是任意长度,任意类型,但是同一个List中的元素类型必须相同。
例如之前已经提到过的[1,2,3],String就是Char的List。
List的元素还可以是List,如
ghci> :type[[True],[False,False]]
[[True],[False,False]] :: [[Bool]]
[[Bool]]类型就表示元素为[bool]的List
Tuples
用一对圆括号表示。固定长度,但是可以包含不同类型的元素。如:
ghci> :type (True,"hello")
(True, "hello") :: (Bool, [Char])
有一个特例是没有任何元素的Tuple,(),它的值与类型都被称为“Unit”,有一些类似于C语言中的void。
Tuple不能只有一个元素。有两个元素的Tuple通常被称为pair。通常,Tuple中不应包含太多的元素。可以通过Tuple从函数返回多个数值。
函数的类型p27
Haskell中的函数类似于数学中的函数,是一个从输入到输出的映射。那么函数的类型其实就是反映了这个映射,如lines函数,
ghci> :type lines
lines :: String -> [String]
它的类型是String -> [String],->读作to,意思可以近似的看做“返回”。光看类型我们就知道,它接受一个字符串,返回由字符串组成的List。
实际调用它看看,确实如上面描述的一样,
ghci> lines "thequick\nbrown fox\njumps"
["the quick","brownfox","jumps"]
函数的纯度 p27
没有副作用(side effects)的函数称为纯函数,有副作用的函数称为非纯函数。
副作用
是指函数行为与系统全局状态之间的关联性,或者说是函数具有不可见的输入或输出。例如,其他语言中读取或者修改全局变量的函数就是有副作用的,尽管这个全局变量并不是函数的参数。而Haskell中的函数默认是没有副作用的,也就是对于相同的输入,无论何时何地都一定会得到相同的输出,就像数学中的函数一样(当然,要与外界打交道的话,必然会引入副作用,Haskell将这一部分都交给了IO模块,之后会提到)。
显然,副作用会让我们函数的行为更加难以预料,也更加难以调试。Haskell这种将纯代码和非纯代码分离的做法使得程序的调试更加方便。
Haskell源文件 p27
Haskell的源文件具有后缀.hs。
之前的示例都是在解释器ghci中进行的,之后的示例将更多的通过读取源文件来进行。
例如在源文件中定义一个函数add,并将源文件命名为add.hs,
add a b = a + b
等号的左边是函数名以及参数名,等号的右边是函数体。
之后可以在ghci中用:load命令载入刚刚保存的源文件,并调用add函数了。
ghci> :load add.hs
[1 of 1] Compiling Main ( add.hs,interpreted )
Ok, modules loaded: Main.
ghci> add 1 2
3
变量 p28
Haskell中,变量是表达式的名字,一旦一个变量绑定(关联)了一个表达式,它的值就不能再被改变。
-- file: ch02/Assign.hs
x = 10
x = 11
载入这个源文件会得到一个错误。
条件求值 p29
Haskell也有自己的if表达式。如:
ghci> if (1==1) then 1 else 0
1
if 关键字引入了一个带有三个部分的表达式:
- 跟在 if 之后的是一个 Bool 类型的表达式,它是 if 的条件部分。
- 跟在 then 关键字之后的是另一个表达式,这个表达式在条件部分的值为 True 时被执行。
- 跟在 else 关键字之后的又是另一个表达式,这个表达式在条件部分的值为 False 时被执行。
我们将跟在 then 和 else 之后的表达式称为“分支”。不同分支的类型必须相同。像是if True then 1 else "foo" 这样的表达式会产生错误,因为两个分支的类型并不相同。
需要注意的是,不同于其他命令式语言,Haskell是一个面向表达式的语言,if表达式不能省略else分支,否则,当条件部分的结果为False时,表达式将没有意义(这里将if表达式看成数学里的分段函数就很好理解了,它们是一个整体,而不是一条条的语句)。
惰性求值 p32
Haskell采用的是惰性求值,也就是表达式在只有真正被用到的时候才求值。如果一个表达式的结果从未被用到,那么这个表达式永远不会被求值。
实际上,未被求值的表达式会存为一个“thunk”(实质是一个临时的闭包?),它保证了在我们需要时,能够求出表达式的值。
一个例子就是||的短路求值,Haskell因为其本身的惰性求值特性,所以是无需额外支持的。
因此,我们可以轻松定义一个自己的短路求值的或函数,
-- file: ch02/shortCircuit.hs
newOr a b = if a then a else b
Haskell中的多态 p36
Haskell中的List就是多态的,因为它的元素可以是任意类型。那么接收一个List作为参数的函数应当是什么类型的呢?Haskell中有个用来取出List中最后一个元素的函数,叫last。
它的类型签名为:
ghci> :type last
last :: [a] -> a
a是一个类型变量,代表着它可以是任意的类型。因为类型签名中包含这样的类型参数,所以last这个函数也是多态的。这种多态也被称为参数多态,C++的模板和它有些类似,C#与Java中泛型的设计也深受Haskell参数多态的影响。
接受多个参数的函数 p38
以take函数为例,
ghci> :type take
take :: Int -> [a] -> [a]
即使我们不知道这个函数具体是做啥的,也能从它的类型签名中略窥一二。因为->是右结合的,这个签名等价于Int -> ([a] -> [a]),看起来它接受一个Int类型的参数,返回一个函数,这个函数接受一个List作为参数,并返回一个List。再根据这个函数的名字take,我们大致可以猜测出这个函数能从一个List中取出一定数量的字符组成另一个List。
这是完全正确的,虽然看起来能接受多个参数,但是实际上Haskell中所有函数都只有一个参数。这种技术称为Currying,在后面会对此有更详细的说明。
为了方便,我们在写类型签名的时候,可以简单的将所有参数及返回值的类型按顺序用->连起来即可。
本章到此结束,这章本来提到了递归,我准备将其放到第4章一起来总结。