类型的作用
在计算机的最底层,处理的都是没有任何附加结构的字节(byte)。而类型系统在这个基础上提供了抽象:它为那些单纯的字节加上了意义,使得我们可以说“这些字节是文本”,“那些字节是机票预约数据”,等等。
通常情况下,类型系统还会在标识类型的基础上更进一步:它会阻止我们混合使用不同的类型,避免程序错误。比如说,类型系统通常不会允许将一个酒店预约数据当作汽车租凭数据来使用。
引入抽象的使得我们可以忽略底层细节。举个例子,如果程序中的某个值是一个字符串,那么我不必考虑这个字符串在内部是如何实现的,只要像操作其他字符串一样,操作这个字符串就可以了。
类型系统的一个有趣的地方是,不同的类型系统的表现并不完全相同。实际上,不同类型系统有时候处理的还是不同种类的问题。
除此之外,一门语言的类型系统,还会深切地影响这门语言的使用者思考和编写程序的方式。而 Haskell 的类型系统则允许程序员以非常抽象的层次思考,并写出简洁、高效、健壮的代码。
<以上摘抄自”Real World Haskell”>
Haskell类型系统
Haskell中的类型有三个有趣的方面: 强类型,静态,可以通过自动推导得出.
强类型
Haskell的强类型系统会拒绝执行任何没有意义的表达式,保证程序不会因为这些表达式引起错误:比如把整数当做函数来使用,或者将一个字符串传给一个只接受整数参数的函数,等等.
Haskell强类型系统的另一个作用是,它不会自动的将值从一个类型转换到另一个类型.比如将一个整数值作为参数传给一个只接受浮点数的函数,C编译器会自动且静默的将整数类型转换为浮点类型,而Haskell编译器则会引起一个编译期错误.
要在Haskell中进行类型转换必须显式的使用类型转换函数.
强类型的最大好处是可以让bug在代码实际运行之前显现.
静态类型
静态类型系统指的是,编译器可以在编译期知道每个值和表达式的类型.Haskell编译器或解释器会觉察出类型不正确的表达式,并拒绝执行:
Prelude> True && "False"
<interactive>:2:9:
Couldn't match expected type `Bool' with actual type `[Char]'
In the second argument of `(&&)', namely `"False"'
In the expression: True && "False"
In an equation for `it': it = True && "False"
类型推导
Haskell编译器可以自动推断出程序中几乎任何表达式的类型,这个过程被称为类型推导(type inference).
正确理解类型系统
常用的基本类型
Char:单个Unicode字符
Bool:布尔逻辑值
Int:带符号的定长整数,由机器决定
Integer:不限长度的带符号整数,由内存量决定
Double:浮点数,由机器决定
:: 符号可表示类型,同时用于类型签名,exp::T拜师exp的类型是T,而::T是变量exp的类型签名.
调用函数
要调用一个函数,首先是函数名,空格后跟参数,并不需要小括号,多个参数间用空格隔开:
Prelude> compare 2 3
LT
在表达式过于复杂时使用括号进行标注:
Prelude> compare (sqrt 3) (sqrt 6)
LT
复合数据类型:列表和元祖
head函数取出列表的第一个元素:
ghci> head [1,2,3]
1
tail函数取出除了第一个元素的所有元素:
ghci> tail [1,2,3,4]
[2,3,4]
[Char]表示一个Char类型的列表,[Int]表示一个整数类型的列表,同理[MyType]表示一个自定义类型的列表.
列表可以任意长度,但是只能表示一种类型.
元祖长度固定,但可以包含不同类型的值.
Prelude> :type (True, "hello")
(True, "hello") :: (Bool, [Char])
特殊类型()表示包含零个元素的元祖,相当于C中的void.
元组的类型由它所包含元素的数量、位置和类型决定。这意味着,如果两个元组里都包含着同样类型的元素,而这些元素的摆放位置不同,那么它们的类型就不相等,就像这样:
Prelude> :type (False, 'a')
(False, 'a') :: (Bool, Char)
Prelude> :type ('a', False)
('a', False) :: (Char, Bool)
只有元组中的数量、位置和类型都完全相同,这两个元组的类型才是相同的:
Prelude> :t (False, 'a')
(False, 'a') :: (Bool, Char)
Prelude> :t (True, 'b')
(True, 'b') :: (Bool, Char)
元组通常用于以下两个地方:
- 如果一个函数需要返回多个值,那么可以将这些值都包装到一个元组中,然后返回元组作为函数的值。
- 当需要使用定长容器,但又没有必要使用自定义类型的时候,就可以使用元组来对值进行包装。
处理列表和元组的函数
函数 take 和 drop 接受两个参数,一个数字 n 和一个列表 l 。
take 返回一个包含 l 前 n 个元素的列表:
Prelude> take 2 [1, 2, 3, 4, 5]
[1,2]
drop 则返回一个包含 l 丢弃了前 n 个元素之后,剩余元素的列表:
Prelude> drop 2 [1, 2, 3, 4, 5]
[3,4,5]
函数 fst 和 snd 接受一个元组作为参数,返回该元组的第一个元素和第二个元素:
Prelude> fst (1, 'a')
1
Prelude> snd (1, 'a')
'a'
将表达式传给函数
Haskell 的函数应用是左关联的。比如说,表达式 a b c d 等同于 (((a b) c) d) 。要将一个表达式用作另一个表达式的参数,那么就必须显式地使用括号来包围它,这样编译器才会知道我们的真正意思:
Prelude> head (drop 4 "azety")
'y'
drop 4 “azety” 这个表达式被一对括号显式地包围,作为参数传入 head 函数。
函数类型
使用 :type 命令可以查看函数的类型[缩写形式为 :t ]:
Prelude> :type lines
lines :: String -> [String]
符号 -> 可以读作“映射到”,或者”返回”,表示函数lines接收一个字符串返回一个字符串列表.
纯度
副作用指的是,函数的状态受系统的全局状态所影响.副作用实质上是一种不可见的输入输出.Haskell的函数在默认情况下都是无副作用的,函数的结果只取决于显示传入的参数.
我们将带副作用的函数叫做”不纯(impure)函数”,而不带副作用的函数叫做”纯(pure)函数”.
从类型签名可以看出一个 Haskell 函数是否带有副作用 —— 不纯函数的类型签名都以 IO 开头:
Prelude> :type readFile
readFile :: FilePath -> IO String
Haskell源码,简单函数的定义
编辑add.hs文件:
-- file: ch02/add.hs
add a b = a + b
等号左边的add a b是函数名和参数,而右边的a + b是函数体,符号 = 表示将左边的名字定义为右边的表达式.
将add.hs文件保存后可以在ghci中通过:load(缩写:l)载入,然后就可以像使用其他函数一样调用add函数了.
Haskell不需要return关键字来返回函数值,因为一个函数就是一个单独的表达式,而不是一组语句.
变量
在Haskell中,可以使用变量来赋予表达式名字:一旦变量绑定了某个表达式,那么这个变量的值就不会改变,我们总能用这个变量来指代它所关联的表达式,并且每次都会得到同样的结果.
与命令式编程语言不同的是,命令式编程语言中,一个变量通常用于标识一个内存位置,并且在任何时候都可以修改这个变量的值.因此在不同的时间点访问这个变量都可能得到不同的值.
条件求值
列表的drop函数,当第一个参数小于0时,drop函数返回整个输入列表,否则他就从列表左侧逐个移除元素,一直到移除的元素数量足够或者输入列表被清空为止.
自己定义一个myDrop函数,用if表达式来决定该做什么,null函数用于检查列表是否为空:
-- file: ch02/myDrop.hs
myDrop n xs = if n <= 0 || null xs
then xs
else myDrop (n - 1) (tail xs)
在if表达式中,多个分支的类型必须相同.在命令式编程中,代码由语句而不是表达式构成,因此省略if语句的else分支时程序仍然有意义.但是当代码由表达式构成时,一个缺少else分支的if语句,在条件为false时,是没有办法给出一个结果的,当然也就没有类型,因此省略else分支在Haskell中会导致表达式无意义,引起编译期异常.
通过实例了解求值
惰性求值
mod函数是典型的取模函数:
-- file: ch02/isOdd.hs
isOdd n = mod n 2 == 1
在使用严格求值的语言里,函数的参数总是在应用函数之前被求值.在函数isOdd中,自表达式 mod n 2 首先被求值,即对参数取模,然后将结果赋予n,最后将n于1进行比较,相等则得到结果True,即奇数.
Haskell使用了另外一种求值方式-非严格求值.在这种情况下,isOdd (1 + 2)并不会使得子表达式 1 + 2立即被求值为3,相反,编译器做出了一个承诺说,当真正需要的时候,我有办法计算出isOdd (1 + 2)的值.
用于追踪未求值表达式的记录被称为块(thunk).这是实情发生的经过:编译器通过创建块来延迟表达式的求值,直到这个表达式的值真正被需要为止.如果这个表达式的值始终不被需要,那么这个表达式始终不会被求值.
一个更复杂的例子
现在考察myDrop 2 “abcd”的结果是如何被计算的:
Prelude> :load "myDrop.hs"
[1 of 1] Compiling Main ( myDrop.hs, interpreted )
Ok, modules loaded: Main.
*Main> myDrop 2 "abcd"
"cd"
当执行表达式myDrop 2 “abcd”时,函数myDrop应用于值2和”abcd”,变量n被赋予2,变量xs被赋予”abcd”,将两个变量代换到myDrop的条件判断部分,就得出了以下表达式:
*Main> :type 2 <= 0 || null "abcd"
2 <= 0 || null "abcd" :: Bool
编译器需要对 2 <= 0 || null “abcd”进行求值,从而决定if该执行哪个分支,这需要对(||)进行求值,而需要求值这个表达式,又需要对他的左操作符进行求值:
*Main> 2 <= 0
False
将False代换到(||)表达式当中,得出以下结论:
*Main> :type False || null "abcd"
False || null "abcd" :: Bool
如果(||)的左操作符为True,那么(||)就不需要对右操作符进行求值.因为此处左操作符的值为False,则整个表达式的值由右操作符决定:
*Main> null "abcd"
False
最后将两个值代换到表达式中:
*Main> False || False
False
这个结果表明,下一步要求值的if表达式的else分支,而这个分支包含一个对myDrop函数自身的递归调用:myDrop (2 - 1) (tail “abcd”) .
递归
当递归调用myDrop时,n被绑定为(2 - 1),而xs被绑定为 tail “abcd”,于是再次对myDrop进行求值,这次将新的值代换到if条件的判断部分:
*Main> :type (2 - 1) <= 0 || null (tail "abcd")
(2 - 1) <= 0 || null (tail "abcd") :: Bool
对 (||) 的左操作符的求值过程如下:
*Main> :type (2 - 1)
(2 - 1) :: Num a => a
*Main> 2 - 1
1
*Main> 1 <= 0
False
子表达式(2 - 1)只有在真正需要的时候才会被求值,同样对右操作符(tail “abcd”)的求值也会被延迟,直到真正有需要时才会被执行:
*Main> :type null (tail "abcd")
null (tail "abcd") :: Bool
*Main> tail "abcd"
"bcd"
*Main> null "bcd"
False
因为条件判断表达式的最终结果为False,所以这次执行的也是else分支,而被执行的表达式为 myDrop (1 - 1) (tail “bcd”) .
终止递归
这次递归调用将 1 - 1 绑定到 n ,而 xs 被绑定为 tail “bcd”:
*Main> :type (1 - 1) <= 0 || null (tail "bcd")
(1 - 1) <= 0 || null (tail "bcd") :: Bool
再次对(||)操作符的左对象求值:
*Main> :type (1 - 1) <= 0
(1 - 1) <= 0 :: Bool
最终我们得到了一个True值:
*Main> True || null (tail "bcd")
True
因为 (||) 的右操作符 null (tail “bcd”) 并不影响表达式的计算结果,因此他没有被求值,而整个条件判断部分最终值为True,于是then分支被求值:
*Main> :type tail "bcd"
tail "bcd" :: [Char]
从递归中返回
在求值的最后一步,结果表达式tail “bcd”处于第二次对myDrop的递归调用当中,因此,表达式tail “abcd”作为结果值,被返回给myDrop的第二次递归调用:
*Main> myDrop (1 - 1) (tail "bcd") == tail "bcd"
True
接着,第二次递归调用所得的值,还是tail “bcd”,被返回给第一次递归调用:
*Main> myDrop (2 - 1) (tail "abcd") == tail "bcd"
True
然后,第一次递归调用将tail “bcd”作为结果值返回给myDrop调用:
*Main> myDrop 2 "abcd" == tail "bcd"
True
最终计算出结果:
*Main> myDrop 2 "abcd"
"cd"
*Main> tail "bcd"
"cd"
在从递归调用中退出并传递结果值得过程中,tail “bcd”并不会被求值,只有当他返回到最开始的myDrop后,ghci需要打印这个值时,才会被求值.