Real World Haskell学习篇-第2章: 类型和函数

时间:2021-05-10 17:06:56

1. Haskell的类型系统

  Haskell的类型有3个特性:

  type strong(强类型)

  type static (静态类型)

  auto-inferred (自动推导类型)

  1.1 强类型

  1.   强类型只会执行well typed的类型,不执行ill typed。
  2.   强类型不会进行类型自动转换, 必要时显式地使用类型转换函数。
  3.   强类型可以检测类型错误的bug。

  1.2 静态类型

    编译器在编译期(非执行期)就可识别变量或表达式的类型。编译器会拒绝执行不正确的表达式:

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第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)
View Code

 

    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个元素的后续元素。

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第2章: 类型和函数
 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
View Code

  由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函数了:

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第2章: 类型和函数
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
View Code

  add a b = a + b 函数说明:

  add a b是函数名和函数参数列表, a + b是函数体, = 表示将左边的名字定义为右边的表达式。

  表达式的结果就相当于函数的返回值, Haskell函数的定义, 就是一个表达式而已,不必陈述return语句。

  6.1 变量

    Haskell变量是与表达式相关量的,可以认为是表达式的别名, 可以替换,就像函数式一样。

    所以,如果我们做变量的多次赋值,Haskell会不允许。 比如Assign.hs中, x = 10;x = 11 :

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第2章: 类型和函数
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.
View Code

  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看下结果先:

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第2章: 类型和函数
 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"
View Code

  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函数来说明参数多态

Real World Haskell学习篇-第2章: 类型和函数Real World Haskell学习篇-第2章: 类型和函数
1 GHCi> last [1,2,3,4,5]
2 5
3 GHCi> last "abc"
4 'c'
5 GHCi> :t last
6 last :: [a] -> a
View Code

    [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默认使用纯函数。这意味着它不会做类似读取文件,访问网络,返回当前时间等操作。