静态类型

Haskell是静态类型(Static Type)语言,在编译时期每个表达式的类型都已经确定下来。如果在代码中有类型错误,就不可能通过编译。这极大地提高了代码的安全性。在Haskell中,所有东西都有类型。

GHCi中查看数据类型

在GHCi中,通过 :t <name> 或者 :type <name>来查看变量、常量或表达式的数据类型。例如:

Prelude> let a = 1
Prelude> :t a
a :: Num a => a
Prelude> :type True
True :: Bool
Prelude> :t 4 == 5
4 == 5 :: Bool

基本数据类型

Haskell有以下几种基本数据类型:

  • Char 单个Unicode字符。
  • Bool 表示一个Bool逻辑值。这个类型只有两个值:TrueFalse
  • Int 表示一个整数。范围为-2147483648 ~ 2147483647
  • Integer 可以认为是无限范围的整数。
  • Floating 表示浮点数。注意**运算得到的结果的数据类型是Floating

List

在Haskell中,一个List由一对方括号括起来。其中的元素的数据类型必须相同,相邻元素之间用逗号,隔开。

Haskell的字符串

在Haskell中,字符串实际上是一组字符的List,例如,"Hello"只是['H', 'e', 'l', 'l', 'o']的语法糖而已。

Prelude> :t "Hello"
"Hello" :: [Char]

List的拼接

两个List之间的拼接用++运算符:

Prelude> [1, 2] + [3, 4]
[1, 2, 3, 4]

注意++符号会遍历整个++符号左边的List,因此,当给较长的List追加元素时,会有严重的性能问题。

向List中追加元素

:运算符的作用是在一个List的前面追加一个元素:

Prelude> 1: [2, 3, 4]
[1, 2, 3, 4]

:运算符无法在List的后面追加元素,例如,以下用法会出现错误:

Prelude> [2, 3, 4] : 1

<interactive>:77:1
    No instance for (Num [[t0]]) arising from a use of 'it'
    In a stmt of an interactive GHCi command: print it

如果需要在List的后面追加元素,可以使用如下的语法:

Prelude> [2, 3, 4] ++ [1]
[2, 3, 4, 1]

即把要追加的元素写成一个单独的List,再用 ++运算符将两个List连接。

多级List

List中的元素可以是List,其长度可以不同,但其中的元素的数据类型必须是相同的。

List索引元素

使用 !!按照索引取得List中的元素,例如:

Prelude> [1, 2, 3] !! 1
2

List的索引值是从 0 开始的。如果索引值超过了List的长度,便会出错。

对于多级列表的索引,只需要逐级索引即可,例如:

Prelude> let a = [[1, 2, 3], [4, 5], [6]]
Prelude> a !! 1 !! 1
5

List大小比较

当List中装有课比较的元素时,可以用>运算符和>=运算符以及<运算符和<=运算符比较List的大小。用来比较大小的两个List中的元素的数据类型必须是相同的。它会先比较第一个元素,如果相等,在比较下一个元素。如果直到其中一个List已经到达末尾其对应位置的元素仍相等,而另一个没有到达末尾,则另一个List较大。举例:

Prelude> [2, 2] > [2]
True
Prelude> [1, 2] < [2]
True

List相关的常用函数

  • head 返回一个List的头部(首个元素)。如果当前List为空,则会产生异常。
Prelude> head [1, 2, 3]
1
Prelude> head []
*** Exception: Prelude.head: empty list
  • tail 返回一个List除去头部元素之后的元素的List。如果当前List为空,则会产生异常。
Prelude> tail [1, 2, 3]
[2, 3]
Prelude> tail [1]
[]
Prelude> tail []
*** Exception: Prelude.tail: empty list
  • last 返回一个List的最后一个元素。

  • init 返回一个List的除去最后一个元素的部分(List)。

  • length 返回一个List的长度(包含的元素个数)。

  • null 检查一个List是否为空,若为空,返回True,否则返回False

  • reverse 将一个List反转。

  • take 取得一个List的前几个元素。

Prelude> take 2 [1, 2, 3]
[1, 2]
Prelude> take 1 [1, 2, 3]
[1]

如果要求的元素个数大于List的长度,则返回整个List,不会产生异常。

Prelude> take 4 [1, 2]
[1, 2]
  • maximum 取得一个List中的元素的最大值。

  • minimum 取得一个List中的元素的最小值。

  • sum 求得一个List中所有元素的和。注意用于求和的List中的元素必须是可相加的类型。例如,其元素类型不能是Char。对空List的求和结果为0

  • elem 判断一个元素是否在List中。如果在,返回True,否则返回False。也可以用中缀函数的方法调用elem函数。

Prelude> elem 1 [1, 2, 3]
True
Prelude> 4 `elem` [1, 2, 3]
False

List 与 Range

对于可枚举的值,通过区间(Range)的方式可以很方便地生成列表。比如,要生成一个包含1-20的自然数的列表,只需要运行如下命令:

Prelude> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

对于字符型列表的生成,Haskell会按照字符的ASCII码的顺序来生成。

Prelude> ['a'..'d']
"abcd"
Prelude> ['a'..'a']
"a"

如果给出的上界大于下界,则返回一个空List。

Prelude> ['b'..'a']
""
Prelude> [1..0]
[]

通过Range的生成方式可以指定其规律,例如:

Prelude> [2, 4..10]
[2,4,6,8,10]

Haskell正是根据前两项推导出生成规则的。

Range也可以生成无限长的列表,不指定上界即可:

Prelude> [2, 4..]

在GHCi中执行这条命令,便会不停输出一个无限长的列表[2,4,6,8,10 ...]

由于Haskell是惰性的,因此,也可以通过这这种方式来得到List的前几项:

Prelude> take 3 [2, 4..]
[2, 4, 6]

List与cycle函数

cycle函数的作用是无限重复某一列表的内容:

Prelude> take 5 (cycle [1, 2, 3])
[1, 2, 3, 1, 1]

List 与 repeat 函数

repeat函数的作用是接受一个值作为参数,并返回一个仅包含该值的无限List。

Prelude> take 5 (repeat 2)
[2,2,2,2,2]

replicate函数也具有同样的功能:

Prelude> replicate 3 10
[10, 10, 10]

List推导式(List Comprehension)

在Haskell中,List Comprehension的核心思想是通过不断添加谓词(predicate)(限制条件)来从一个集合中不断筛选出符合条件的元素,最终得到想要的集合。

Prelude> [x | x <- [10 .. 20], odd x]
[11, 13, 15, 17, 19]
Prelude> [x*2 | x <- [10 .. 20], even x, x /= 16]
[10, 12, 14, 18, 20]

注意:多个谓词(predicate)之间用”,”连接。当有多个谓词条件时,会组合多个谓词的结果:

Prelude> [(i,j) | i <- [1,2], j <- [1..4]]
[(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4)]

基于列表推导的特性,我们很容易写出一个枚举序列的n-组合的程序:

import Data.List

combinations :: Int -> [a] -> [[a]]
combinations 0 _ = [[]]
combinations n xs = [y:ys | y:xs' <- tails xs, ys <- combinations (n-1) xs']

其中用到了Data.List中的tails方法:

tails :: [a] -> [[a]] Source

The tails function returns all final segments of the argument, longest first. For example,

tails "abc" == ["abc", "bc", "c",""]

combinations函数的使用:

Prelude> combinations 3 "abcdef"
["abc","abd","abe", ...]

Tuple

Tuple的特点

在Haskell中,元组用”()”来表示,元组中的不同元素之间用”,”分隔开。通常用元组来表示向量等数据结构。使用元组(Tuple)时应该注意以下几点问题:

  • 每一个元组中的元素可以是不同类型的元素,例如:

      Prelude> ('a', 1, "abcd")
      ('a',1,"abcd")
    
  • 不同长度的元组是不同的类型
  • 同样长度,对应位置元素类型相同,且元素可比较的Tuple是可以比较大小的。
  • 元组中元素的最小长度为2,因此不存在只有一个元素的元组,但可以存在只有一个元素的List。

序对(Pair)

Haskell中,序对(Pair)是只有两个元素的元组。

  • fst函数返回一个序对的首项。
  • snd函数返回一个序对的尾项。
Prelude> fst (8, 11)
8
Prelude> snd (8, 11)
11

注意fst函数和snd函数仅仅对序对(Pair,只有两个元素的元组)有效。

zip 函数

zip函数把两个List交叉配对,生成一个Pair的List。如果两个List长度不同,以较短的List的长度为基准,较长List中多余的项舍去。

Prelude> zip [1, 2, 3] "abcde"
[(1,'a'),(2,'b'),(3,'c')]

类型变量与多态

在GHCi中运行如下命令,会看到:

Prelude> :t head
head :: [a] -> a

此处,a便是一个类型变量(Type variables),它可以是任何类型。使用了类型变量的函数是多态函数。Haskell中,类似的函数还有:

head, tail, sum, fst, snd ... 等等。

类型类(Typeclasses)

Haskell中,类型类相当于提供了一系列的接口,属于某一类型类的数据类型具有对应的一类性质。

主要有以下几种类型类:

  • Eq类型类

Eq类型类用于可以判断相等性的类型。其实例必须实现==/=这两个函数。

  • Ord类型类

Ord类型类用于可以比较大小的类型。Ord类型中包含了所有的比较函数。

- `>`、`<`、`>=`、`<=`。
- `compare`函数。`compare` 函数读取连个Ord中的相同类型的值作为参数,返回一个Ordering类型的值。Ordering类型有 `GT`、`LT`、`EQ`三种值,分别表示大于、小于和等于。

举例:

Prelude> compare 1 2
LT
Prelude> 1 `compare` 2
LT
  • Show 类型类

Show类型类的实例为可以表示为字符串的类型。

Prelude> :t show
show :: Show a => a -> String
  • Read类型类

Read类型类与Show类型类相反。read函数可以取一个字符串作为参数并转为Read的某个实例的类型,其具体类型可以在表达式中自动推断。

Prelude> read "True" || True
True
Prelude> read "1" + 1
2
Prelude> read "[1, 2, 3]" ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]

注意:read函数不能将字符串解析成字符串。

Prelude> read "123" ++ "456"
*** Exception: Prelude.read: no parse

除了自动推断类型以外,还可以通过类型注解(type annotation)的方式显示指出应该将字符串解析成何种类型。类型注解跟在表达式后面,通过::连接。

Prelude> read "123" :: Int
123
Prelude> read "123" :: Float
123.0

如果将read函数放在列表中,便可以根据这个列表中其他元素的类型来解析得到对应的类型。

Prelude> [read "123", 4]
[123, 4]
Prelude> [read "123", 4.0]
[123.0, 4.0]
  • Enum 类型类

Enum类型类的实例类型都是有连续顺序的,都是可枚举的。对于Enum类型的实例类型,每个值都有相应的后继(successer)和前驱(predecesor)。可以用succ函数和pred函数得到。该类型类包含的主要类型有:

(), Bool, Char, Ordering, Int, Integer, Float 和 Double。
  • Bounded类型类

Bounded类型类的实例类型都有一个上限和下限,分别可以通过maxBoundminBound函数得到。

Prelude> minBound :: Int
-2147483648
Prelude> maxBound :: Bool
True
Prelude> maxBound :: Char
'\1114111'
Prelude> maxBound :: (Bool, Int, Char)
(True, 2147483647, '\1114111')
  • Floating类型类

Floating类型类用于存储浮点数,仅包含Float和Double两种浮点类型。

  • Integral类型类

Integral类型类仅包含整数,其实力类型有Int和Integer等。

haskell中的类型转换

前面提到的Read类可以通过自动推断或类型注解的方式将字符串转换成其他类型。Haskell同时还提供了在不同类型之间进行转换(type case)的方法。

例如:fromInteger函数可以将Integer类型的数据转换成其他类型。

func :: (Floating a) => a -> a
func a = a * 2.0

main :: IO()
main = do
    let a = read "20" :: Integer
    print $ func $ fromInteger a

还可以通过与read函数类似的加注解的方式来指定fromInteger函数将Integer类型的变量转换为哪一种类型而非自动类型推断。

main :: IO()
main = do
    let a = read "1234" :: Integer
    print $ (fromInteger a :: Double)

代码运行后,将输出:

1234.0

Standard classes

Haskell的类型继承图:

Haskell Classes