Real World Haskell 读书笔记(2)类型与函数

时间:2022-10-08 17:01:44

Real World Haskell 读书笔记(2)类型与函数

为什么要关心类型? p17

在Haskell中,所有的表达式与函数都有类型。

类型给予一组字节以意义,告诉程序这些字节应该以什么方式组织起来。同时类型也提供了一层抽象,让我们不用关心它底层的细节,而只用关心它是个Int还是个String或是什么。当然类型系统除了抽象之外,还提供类型检查等功能。

 

Haskell的类型系统 p18

Haskell的类型系统是strongstatic的,并且可以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章一起来总结。