1. Haskell的类型系统
Haskell的类型有3个特性:
type strong(强类型)
type static (静态类型)
auto-inferred (自动推导类型)
1.1 强类型
- 强类型只会执行well typed的类型,不执行ill typed。
- 强类型不会进行类型自动转换, 必要时显式地使用类型转换函数。
- 强类型可以检测类型错误的bug。
1.2 静态类型
编译器在编译期(非执行期)就可识别变量或表达式的类型。编译器会拒绝执行不正确的表达式:
1 GHCi> True && "False"
2
3 <interactive>:8:9:
4 Couldn't match expected type ‘Bool’ with actual type ‘[Char]’
5 In the second argument of ‘(&&)?? namely ‘"False"’
6 In the expression: True && "False"
7 In an equation for ‘it’: it = True && "False"
8 (0.03 secs, 8,416,164 bytes)
Haskell提供的type class机制安全,方便,实用,并提供大部分动态类型的优点,也提供了一部分对全动态类型(truly dynamic type)编程的支持。
1.3 类型推导
Haskell编译器可以自动推断出几乎所有表达式的类型。
1.4 正确理解类型系统
上面的强类型和静态类型使Haskell更安全,类型推导是Haskell更精炼,简洁。因为这相当于在做类型检查工作,提前做了调试工作。
1.5 常用的基本类型
Char: 单个Unicode字符
Bool:布尔类型 True,False 不是等同于1和0.
Int: 带符号的定长整数。 Haskell中不少于28 bit。
Integer: 不限长度的带符号整数。 需要更多地内存和计算量。不会造成溢出。 更可靠。
Double: 浮点数。通常是64 bit。Haskell不推荐使用Float类型, 这会使计算较慢。
特别说明: :: 符号
第一表示类型,第二表示类型签名。例如: exp::T 及表示exp的类型是T,::T就是表达式exp的类型签名。如果表达式未显式地指明类型,那么他的类型就会通过自动推导决定:
1 GHCi> :type 'a'
2 'a' :: Char
3 GHCi> 'a'
4 'a'
5 (0.00 secs, 0 bytes)
6 GHCi> 'a'::Char
7 'a'
8 (0.00 secs, 0 bytes)
2. 调用函数
先写函数名,再写参数列表, 用空格隔开。
函数的优先级要高于操作符。(第10行)
1 True
2 GHCi> odd 6
3 False
4 GHCi> compare 3 4
5 LT
6 GHCi> compare 3 3
7 EQ
8 GHCi> compare 5 3
9 GT
10 GHCi> compare 2 3 == LT
11 True
为了便于可读, 必要的()也要加上。
1 GHCi> compare (sqrt 3) (sqrt 6)
2 LT
3 GHCi> compare sqrt 3 sqrt 6
4
5 <interactive>:21:1:
6 Couldn't match expected type ‘(Double -> Double) -> Integer -> t’
7 with actual type ?甇rdering’
8 Relevant bindings include it :: t (bound at <interactive>:21:1)
9 The function ‘compare’ is applied to four arguments,
10 but its type ‘(a0 -> a0) -> (a0 -> a0) -> Ordering’ has only two
11 In the expression: compare sqrt 3 sqrt 6
12 In an equation for ‘it’: it = compare sqrt 3 sqrt 6
3. 复合数据类型: 列表和元组
复合类型是由其它类型构建而成。如 String == [Char]
head函数取列表的第1个元素, tail函数取除第1个元素的后续元素。
1 GHCi> head ['a','b','c']
2 'a'
3 GHCi> head "list"
4 'l'
5 GHCi> head []
6 *** Exception: Prelude.head: empty list
7 GHCi> tail [1,2,3,4]
8 [2,3,4]
9 GHCi> tail "list"
10 "ist"
11 GHCi> tail []
12 *** Exception: Prelude.tail: empty list
由head,tail函数可知,它们可以处理不同类型的列表,所以成列表是类型多态的。
当需要编写带有多态类型的code,需要使用类型变量。这些类型变量以小写字母开头,作为一个占位符, 最终被一个具体类型替代。
比如 [a]表示类型为a的列表, [Int]表示一个包含Int类型值的列表。
也可以递归替换。[[Int]]是一个包含[Int]类型值的列表。
元组
如果我们需要保存不同类型的数值时,就可以使用元组。
列表与元组的区别
列表可以任意长度, 且只包含相同类型, 用[]包含。
元组只能固定长度, 可以包含不同类型, 用()包含。
1 GHCi> (4,"Hello",(12,True),['a','b'])
2 (4,"Hello",(12,True),"ab")
3 GHCi> ()
4 ()
Haskell有一个特殊的元组(),即包含0个元素的元组, 相当于C中的void。
通常用元组中元素的数量称呼元组的前缀。 比如 5-元组 称呼 包含5个元素的元组。 不可以创建包含一个元素的元组。 (1-元组)
元组的类型
只有当元组中元素的数量,位置,类型都相同时,才称为相同的元组类型:
1 GHCi> :t (False,'a')
2 (False,'a') :: (Bool, Char)
3 GHCi> :t (True, 'b')
4 (True, 'b') :: (Bool, Char)
元组的用途
当函数需要返回多个值时。
当需要使用定长容器,又不需要自定义类型。
4. 处理列表和元组的函数
take n l : 返回一个包含列表l中前n个元素的列表
drop n l : 返回一个丢弃列表l中前n个元素的列表
1 GHCi> take 2 [1,2,3,4,5] 2 [1,2] 3 GHCi> drop 3 [1,2,3,4,5] 4 [4,5]
fst (...) : 返回元组中的第1个元素
snd(...) : 返回元组中的第2个元素
1 GHCi> fst ([1,2,4],True) 2 [1,2,4] 3 GHCi> snd ([1,2,4],True) 4 True
4.1 将表达式传给函数
要恰当的使用()传递参数
1 GHCi> head (drop 4 "dfrtu")
2 'u'
3 GHCi> head "u"
4 'u'
4.2 函数类型
可以用:type命令查看函数类型, 简写:t
1 GHCi> :type lines 2 lines :: String -> [String]
符号 -> 表示“映射到”, 也可以认为是返回。
String -> [String]: 表示lines函数定义了一个String到[String]的函数映射。
lines函数的类型签名表示, lines可以用单个字符串作为参数, 返回一个包含多个字符串的列表:
1 GHCi> lines "the quick\nbrown fox\njumps"
2 ["the quick","brown fox","jumps"]
3 GHCi> lines ("dfetrt")
4 ["dfetrt"]
5 GHCi> lines ("dfetrt" ++ "\nhere")
6 ["dfetrt","here"]
5. 纯度
带副作用的函数称为不纯函数(impure), 不带副作用的函数称为纯函数(pure)。
副作用指的是, 函数的行为受全局状态的影响。比如:
一个函数要读取并返回某个全局变量, 如果程序中其它代码可以修改全局变量,那么这个函数的返回值取决于这个全局变量某一时刻的返回值。 我们说这个函数带有副作用。
副作用本质上就是一个函数不可见的(invisiable)输入或输出。 Haskell的函数默认都是无副作用的: 函数的结果只取决于显示传入的参数。
不纯函数的类型签名都以IO开头:
1 GHCi> :type readFile 2 readFile :: FilePath -> IO String
6. Haskell源码及简单函数的定义
Haskell源码以.hs为后缀。 在ghci中定义函数和Haskell源码里定义有些差异。这里创建一个最简单的add函数;
使用:cd,使ghci切换到add.hs的当前目录;
使用:load(省略为:l)加载add.hs文件;
OK,现在可以调用add函数了:
1 GHCi> :cd F:\Haskell\projs
2 Warning: changing directory causes all loaded modules to be unloaded,
3 because the search path has changed.
4 GHCi> :l add.hs
5 [1 of 1] Compiling Main ( add.hs, interpreted )
6 Ok, modules loaded: Main.
7 GHCi> add 4 5
8 9
add a b = a + b 函数说明:
add a b是函数名和函数参数列表, a + b是函数体, = 表示将左边的名字定义为右边的表达式。
表达式的结果就相当于函数的返回值, Haskell函数的定义, 就是一个表达式而已,不必陈述return语句。
6.1 变量
Haskell变量是与表达式相关量的,可以认为是表达式的别名, 可以替换,就像函数式一样。
所以,如果我们做变量的多次赋值,Haskell会不允许。 比如Assign.hs中, x = 10;x = 11 :
1 GHCi> :l Assign.hs
2
3 Assign.hs:2:1:
4 Multiple declarations of ‘x’
5 Declared at: Assign.hs:1:1
6 Assign.hs:2:1
7 [1 of 1] Compiling Main ( Assign.hs, interpreted )
8 Failed, modules loaded: none.
6.2 条件求值
利用Haskell的if then else 语句来实现自己的drop函数功能(myDrop.hs):
1 myDrop n xs = if n <= 0 || null xs
2 then xs
3 else myDrop (n-1) (tail xs)
null是判断xs列表是否为空,如: null []。 then else分别执行if条件为True,False的情况,称为2路分支,且分支的类型必须相同,且else不可或缺。
:l myDrop.hs看下结果先:
1 GHCi> drop 2 "foobar"
2 "obar"
3 GHCi> drop 4 [1,2]
4 []
5 GHCi> drop 0 [1,2]
6 [1,2]
7 GHCi> drop (-2) "foo"
8 "foo"
9 GHCi> :l myDrop.hs
10 [1 of 1] Compiling Main ( myDrop.hs, interpreted )
11 Ok, modules loaded: Main.
12 GHCi> myDrop 2 "foobar"
13 "obar"
14 GHCi> myDrop 4 [1,2]
15 []
16 GHCi> myDrop 0 [1,2]
17 [1,2]
18 GHCi> myDrop (-2) "foo"
19 "foo"
6.3 通过实例了解求值
先来一个通过取模运算判断奇数的例子(isOdd.hs):
1 isOdd n = mod n 2 == 1
非严格求值: 即表达式被求值的时刻。 比如 isOdd (1+2): 并不是先计算1+2的值, 而是将此表达式带入isOdd函数中,在需要求值时才会计算。
用于追踪未求值表达式的记录称为块(thunk)。
非严格求值通常被称为惰性求值, 实际上有差别。
从myDrop.hs的递归函数发现, 函数的返回值可能是一个块。
7. Haskell的多态
7.1 参数多态
我们通过last函数来说明参数多态:
1 GHCi> last [1,2,3,4,5]
2 5
3 GHCi> last "abc"
4 'c'
5 GHCi> :t last
6 last :: [a] -> a
[a]是last函数的参数,a是返回值,->表示右关联。即是传入一个类型变量为a的列表, 返回一个类型变量为a的元素。具体a是什么类型,Haskell不关心。
即是参数多态。
Haskell不支持强制多态(像整型自动转化成浮点型那样)
7.2 多参数函数的类型
take函数接收1个整型,1个列表,共2个参数:
1 GHCi> :t take 2 take :: Int -> [a] -> [a]
从右关联可以看出, take接收Int,类型变量为a的列表, 返回类型变量为a的列表。
8. 纯度
Haskell默认使用纯函数。这意味着它不会做类似读取文件,访问网络,返回当前时间等操作。