网址:http://learnyouahaskell.com/

中文版:http://learnyouahaskell-zh-tw.csie.org/zh-cn/ready-begin.html

在 Haskell 中,List 就像现实世界中的购物单一样重要。它是最常用的数据结构,并且十分强大,灵活地使用它可以解决很多问题。本节我们将对 List,字串和 list comprehension 有个初步了解。 在 Haskell 中,List 是一种单类型的数据结构,可以用来存储多个类型相同的元素。我们可以在里面装一组数字或者一组字符,但不能把字符和数字装在一起。

Note: 在 ghci 下,我们可以使用 let 关键字来定义一个常量。在 ghci 下执行let a =1 与在脚本中编写 a=1 是等价的。

  1. ghci> let lostNumbers = [4,8,15,16,23,48]   
    ghci> lostNumbers   
    [4,8,15,16,23,48] 

如你所见,一个 List 由方括号括起,其中的元素用逗号分隔开来。若试图写 [1,2,'a',3,'b','c',4] 这样的 List,Haskell 就会报出这几个字符不是数字的错误。字串实际上就是一组字符的 List,"Hello" 只是 ['h','e','l','l','o'] 的语法糖而已。所以我们可以使用处理 List 的函数来对字串进行操作。 将两个 List 合并是很常见的操作,这可以通过 ++ 运算符实现。

  1. ghci> [1,2,3,4] ++ [9,10,11,12]   
    [1,2,3,4,9,10,11,12]   
    ghci> "hello" ++ " " ++ "world"   
    "hello world"   
    ghci> ['w','o'] ++ ['o','t']   
    "woot"

在使用 ++ 运算符处理长字串时要格外小心(对长 List 也是同样),Haskell 会遍历整个的 List(++ 符号左边的那个)。在处理较短的字串时问题还不大,但要是在一个 5000 万长度的 List 上追加元素,那可得执行好一会儿了。所以说,用 : 运算符往一个 List 前端插入元素会是更好的选择。

  1. ghci> 'A':" SMALL CAT"   
    "A SMALL CAT"   
    ghci> 5:[1,2,3,4,5]  
    [5,1,2,3,4,5] 

: 运算符可以连接一个元素到一个 List 或者字串之中,而 ++ 运算符则是连接两个 List。若要使用 ++ 运算符连接单个元素到一个 List 之中,就用方括号把它括起使之成为单个元素的 List。[1,2,3] 实际上是 1:2:3:[] 的语法糖。[] 表示一个空 List,若要从前端插入 3,它就成了 [3], 再插入 2,它就成了 [2,3],以此类推。

Note[],[[]],[[],[],[]] 是不同的。第一个是一个空的 List,第二个是含有一个空 List 的 List,第三个是含有三个空 List 的 List。

若是要按照索引取得 List 中的元素,可以使用 !! 运算符,索引的下标为 0。

List 中的 List 可以是不同长度,但必须得是相同的类型。如不可以在 List 中混合放置字符和数组相同,混合放置数值和字符的 List 也是同样不可以的。当 List 内装有可比较的元素时,使用 > 和 >= 可以比较 List 的大小。它会先比较第一个元素,若它们的值相等,则比较下一个,以此类推。

  1. ghci> [3,2,1] > [2,1,0]   
    True   
    ghci> [3,2,1] > [2,10,100]  
  2.  
  3. 常用函数
    head
    tail 返回一个 List 的尾部,也就是 List 除去头部之后的部分。
    last :最后一个
    init:除了最后一个

试一下,若是取一个空 List 的 head 又会怎样?

  1. ghci> head []   
    *** Exception: Prelude.head: empty list 

糟糕,程序直接跳出错误。如果怪兽都不存在的话,那他的头也不会存在。在使用 headtaillast 和 init 时要小心别用到空的 List 上,这个错误不会在编译时被捕获。所以说做些工作以防止从空 List 中取值会是个好的做法。

length:长度

null 检查一个 List 是否为空。如果是,则返回 True,否则返回 False。应当避免使用 xs==[] 之类的语句来判断 List 是否为空,使用 null 会更好。

reverse 将一个 List 反转:

take 返回一个 List 的前几个元素,看:

  1. ghci> take 3 [5,4,3,2,1]   
    [5,4,3]   
    ghci> take 1 [3,9,3]   
    [3]   
    ghci> take 5 [1,2]   
    [1,2]   
    ghci> take 0 [6,6,6]  
    [] 

如上,若是图取超过 List 长度的元素个数,只能得到原 List。若 take 0 个元素,则会得到一个空 List! drop 与take 的用法大体相同,它会删除一个 List 中的前几个元素。

  1. ghci> drop 3 [8,4,2,1,5,6]   
    [1,5,6]   
    ghci> drop 0 [1,2,3,4]   
    [1,2,3,4]   
    ghci> drop 100 [1,2,3,4]   
    []  
  2.  
  3. maximum 返回一个 List 中最大的那个元素。minimun 返回最小的。
    sum 返回一个 List 中所有元素的和。product 返回一个 List 中所有元素的积。

elem 判断一个元素是否在包含于一个 List,通常以中缀函数的形式调用它。

  1. ghci> 4 `elem` [3,4,5,6]   
    True   
    ghci> 10 `elem` [3,4,5,6]   
    False 

这就是几个基本的 List 操作函数,我们会在往后的一节中了解更多的函数。

使用 Range

今天如果想得到一个包含 1 到 20 之间所有数的 List,你会怎么做? 我们可以将它们一个一个用键盘打出来,但很明显地这不是一个完美的方案,特别是你追求一个好的编程语言的时候。我们想用的是区间 (Range)。Range 是构造 List 方法之一,而其中的值必须是可枚举的,像 1、2、3、4...字符同样也可以枚举,字母表就是 A..Z 所有字符的枚举。而名字就不可以枚举了,"john" 后面是谁?我不知道。

要得到包含 1 到 20 中所有自然数的 List,只要 [1..20] 即可,这与用手写[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] 是完全等价的。其实用手写一两个还不是什么大事,但若是手写一个非常长的 List 那就铁定是个笨方法。

  1. ghci> [1..20] 
    [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] 
    ghci> ['a'..'z'] 
    "abcdefghijklmnopqrstuvwxyz" 
    ghci> ['K'..'Z']   
    "KLMNOPQRSTUVWXYZ"

Range 的特点是他还允许你指定每一步该跨多远。譬如说,今天的问题换成是要得到 1 到 20 间所有的偶数或者 3 的倍数该怎样?

  1. ghci> [2,4..20] 
    [2,4,6,8,10,12,14,16,18,20] 
    ghci> [3,6..20] 
    [3,6,9,12,15,18]

仅需用逗号将前两个元素隔开,再标上上限即可。尽管 Range 很聪明,但它恐怕还满足不了一些人对它的期许。你就不能通过[1,2,4..100]这样的语句来获得所有 2 的幂。一方面是因为步长只能标明一次,另一方面就是仅凭前几项,数组的后项是不能确定的。要得到 20 到 1 的 List,[20..1] 是不可以的。必须得 [20,19..1]。 在 Range 中使用浮点数要格外小心!出于定义的原因,浮点数并不精确。若是使用浮点数的话,你就会得到如下的糟糕结果

  1. ghci> [0.1, 0.3 .. 1] 
    [0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

我的建议就是避免在 Range 中使用浮点数。

你也可以不标明 Range 的上限,从而得到一个无限长度的 List。在后面我们会讲解关于无限 List 的更多细节。取前 24 个 13 的倍数该怎样?恩,你完全可以 [13,26..24*13],但有更好的方法: take 24 [13,26..]

由于 Haskell 是惰性的,它不会对无限长度的 List 求值,否则会没完没了的。它会等着,看你会从它那儿取多少。在这里它见你只要 24 个元素,便欣然交差。如下是几个生成无限 List 的函数

cycle 接受一个 List 做参数并返回一个无限 List 。如果你只是想看一下它的运算结果而已,它会运行个没完的。所以应该在某处划好范围。

  1. ghci> take 10 (cycle [1,2,3]) 
    [1,2,3,1,2,3,1,2,3,1] 
    ghci> take 12 (cycle "LOL ") 
    "LOL LOL LOL "

repeat 接受一个值作参数,并返回一个仅包含该值的无限 List。这与用 cycle 处理单元素 List 差不多。

  1. ghci> take 10 (repeat 5) 
    [5,5,5,5,5,5,5,5,5,5]

其实,你若只是想得到包含相同元素的 List ,使用 replicate 会更简单,如 replicate 3 10,得 [10,10,10]

List Comprehension

学过数学的你对集合的 comprehension (Set Comprehension) 概念一定不会陌生。通过它,可以从既有的集合中按照规则产生一个新集合。前十个偶数的 set comprehension 可以表示为

,竖线左端的部分是输出函数,x 是变量,N 是输入集合。在 Haskell 下,我们可以通过类似 take 10 [2,4..] 的代码来实现。但若是把简单的乘 2 改成更复杂的函数操作该怎么办呢?用 list comprehension,它与 set comprehension 十分的相似,用它取前十个偶数轻而易举。这个 list comprehension 可以表示为:

  1. ghci> [x*2 | x <- [1..10]] 
    [2,4,6,8,10,12,14,16,18,20]

如你所见,结果正确。给这个 comprehension 再添个限制条件 (predicate),它与前面的条件由一个逗号分隔。在这里,我们要求只取乘以 2 后大于等于 12 的元素。

  1. ghci> [x*2 | x <- [1..10], x*2 >= 12] 
    [12,14,16,18,20]

cool,灵了。若是取 50 到 100 间所有除7的余数为 3 的元素该怎么办?简单:

  1. ghci> [ x | x <- [50..100], x `mod` 7 == 3] 
    [52,59,66,73,80,87,94]

成功!从一个 List 中筛选出符合特定限制条件的操作也可以称为过滤 (flitering)。即取一组数并且按照一定的限制条件过滤它们。再举个例子 吧,假如我们想要一个 comprehension,它能够使 List 中所有大于 10 的奇数变为 "BANG",小于 10 的奇数变为"BOOM",其他则统统扔掉。方便重用起见,我们将这个 comprehension 置于一个函数之中。

  1. boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]

这个 comprehension 的最后部分就是限制条件,使用 odd 函数判断是否为奇数:返回 True,就是奇数,该 List 中的元素才被包含。

  1. ghci> boomBangs [7..13] 
    ["BOOM!","BOOM!","BANG!","BANG!"]

也可以加多个限制条件。若要达到 10 到 20 间所有不等于 13,15 或 19 的数,可以这样:

  1. ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19] 
    [10,11,12,14,16,17,18,20]

除了多个限制条件之外,从多个 List 中取元素也是可以的。这样的话 comprehension 会把所有的元素组合交付给我们的输出函数。在不过滤的前提 下,取自两个长度为 4 的集合的 comprehension 会产生一个长度为 16 的 List。假设有两个 List,[2,5,10] 和 [8,10,11], 要取它们所有组合的积,可以这样:

  1. ghci> [ x*| x <- [2,5,10], y <- [8,10,11]] 
    [16,20,22,40,50,55,80,100,110]

意料之中,得到的新 List 长度为 9。若只取乘积为 50 的结果该如何?

  1. ghci> [ x*| x <-[2,5,10], y <- [8,10,11], x*> 50] 
    [55,80,100,110]

取个包含一组名词和形容词的 List comprehension 吧,写诗的话也许用得着。

  1. ghci> let nouns = ["hobo","frog","pope"] 
    ghci> let adjectives = ["lazy","grouchy","scheming"] 
    ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns] 
    ["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog", "grouchy pope","scheming hobo", 
    "scheming frog","scheming pope"]

明白!让我们编写自己的 length 函数吧!就叫做 length'!

  1. length' xs = sum [1 | _ <- xs]

_ 表示我们并不关心从 List 中取什么值,与其弄个永远不用的变量,不如直接一个 _。这个函数将一个 List 中所有元素置换为 1,并且使其相加求和。得到的结果便是我们的 List 长度。友情提示:字串也是 List,完全可以使用 list comprehension 来处理字串。如下是个除去字串中所有非大写字母的函数:

  1. removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

测试一下:

  1. ghci> removeNonUppercase "Hahaha! Ahahaha!" 
    "HA" 
    ghci> removeNonUppercase "IdontLIKEFROGS" 
    "ILIKEFROGS"

在这里,限制条件做了所有的工作。它说:只有在 ['A'..'Z'] 之间的字符才可以被包含。

若操作含有 List 的 List,使用嵌套的 List comprehension 也是可以的。假设有个包含许多数值的 List 的 List,让我们在不拆开它的前提下除去其中的所有奇数:

  1. ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]] 
    ghci> [ [ x | x <- xs, even x ] | xs <- xxs] 
    [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

将 List Comprehension 分成多行也是可以的。若非在 ghci 之下,还是将 List Comprehension 分成多行好,尤其是需要嵌套的时候。

Tuple

从某种意义上讲,Tuple (元组)很像 List --都是将多个值存入一个个体的容器。但它们却有着本质的不同,一组数字的 List 就是一组数字,它们的类型相 同,且不关心其中包含元素的数量。而 Tuple 则要求你对需要组合的数据的数目非常的明确,它的类型取决于其中项的数目与其各自的类型。Tuple 中的项 由括号括起,并由逗号隔开。

另外的不同之处就是 Tuple 中的项不必为同一类型,在 Tuple 里可以存入多类型项的组合。

动脑筋,在 Haskell 中表示二维矢量该如何?使用 List 是一种方法,它倒也工作良好。若要将一组矢量置于一个 List 中来表示平面图形又该怎样?我们可以写类似 [[1,2],[8,11],[4,5]] 的代码来实现。但问题在于,[[1,2],[8,11,5],[4,5]] 也是同样合法的,因为其中元素的类型都相同。尽管这样并不靠谱,但编译时并不会报错。然而一个长度为 2 的 Tuple (也可以称作序对,Pair) ,是一个独立的类型,这便意味着一个包含一组序对的 List 不能再加入一个三元组,所以说把原先的方括号改为圆括号使用 Tuple 会 更好: [(1,2),(8,11),(4,5)]。若试图表示这样的图形: [(1,2),(8,11,5),(4,5)],就会报出以下的错误:

  1. Couldn't match expected type `(t, t1)' 
    against inferred type `(t2, t3, t4)' 
    In the expression: (8, 11, 5) 
    In the expression: [(1, 2), (8, 11, 5), (4, 5)] 
    In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)]

这告诉我们说程序在试图将序对和三元组置于同一 List 中,而这是不允许的。同样 [(1,2),("one",2)] 这样的 List 也不行,因为 其中的第一个 Tuple 是一对数字,而第二个 Tuple 却成了一个字串和一个数字。Tuple 可以用来保存多个数据,如,我们要表示一个人的名字与年 龄,可以使用这样的 Tuple: ("Christopher", "Walken", 55)。从这个例子里也可以看出,Tuple 中也可以存储 List。

使用 Tuple 前应当事先明确一条数据中应该由多少个项。每个不同长度的 Tuple 都是独立的类型,所以你就不可以写个函数来给它追加元素。而唯一能做的,就是通过函数来给一个 List 追加序对,三元组或是四元组等内容。

可以有单元素的 List,但 Tuple 不行。想想看,单元素的 Tuple 本身就只有一个值,对我们又有啥意义?不靠谱。(python中可以)

同 List 相同,只要其中的项是可比较的,Tuple 也可以比较大小,只是你不可以像比较不同长度的 List 那样比较不同长度的 Tuple 。如下是两个有用的序对操作函数:

fst 返回一个序对的首项。

  1. ghci> fst (8,11) 
    8 
    ghci> fst ("Wow", False) 
    "Wow"

snd 返回序对的尾项。

  1. ghci> snd (8,11) 
    11 
    ghci> snd ("Wow", False) 
    False

Note:这两个函数仅对序对有效,而不能应用于三元组,四元组和五元组之上。稍后,我们将过一遍从 Tuple 中取数据的所有方式。

  1.  

有个函数很 cool,它就是 zip。它可以用来生成一组序对 (Pair) 的 List。它取两个 List,然后将它们交叉配对,形成一组序对的 List。它很简单,却很实用,尤其是你需要组合或是遍历两个 List 时。如下是个例子:

  1. ghci> zip [1,2,3,4,5] [5,5,5,5,5] 
    [(1,5),(2,5),(3,5),(4,5),(5,5)] 
    ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"] 
    [(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

它把元素配对并返回一个新的 List。第一个元素配第一个,第二个元素配第二个..以此类推。注意,由于序对中可以含有不同的类型,zip 函数可能会将不同类型的序对组合在一起。若是两个不同长度的 List 会怎么样?

  1. ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"] 
    [(5,"im"),(3,"a"),(2,"turtle")]

较长的那个会在中间断开,去匹配较短的那个。由于 Haskell 是惰性的,使用 zip 同时处理有限和无限的 List 也是可以的:

  1. ghci> zip [1..] ["apple", "orange", "cherry", "mango"] 
    [(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

接下来考虑一个同时应用到 List 和 Tuple 的问题:如何取得所有三边长度皆为整数且小于等于 10,周长为 24 的直角三角形?首先,把所有三遍长度小于等于 10 的三角形都列出来:

  1. ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

刚才我们是从三个 List 中取值,并且通过输出函数将其组合为一个三元组。只要在 ghci 下边调用 triangle,你就会得到所有三边都小于等于 10 的三角形。我们接下来给它添加一个限制条件,令其必须为直角三角形。同时也考虑上 b 边要短于斜边,a边要短于 b 边情况:

  1. ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]

已经差不多了。最后修改函数,告诉它只要周长为 24 的三角形。

  1. ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24] 
    ghci> rightTriangles' 
    [(6,8,10)]

得到正确结果!这便是函数式编程语言的一般思路:先取一个初始的集合并将其变形,执行过滤条件,最终取得正确的结果。

  1.  

haskell趣学指南笔记1的更多相关文章

  1. [2017.02.21] 《Haskell趣学指南 —— Learning You a Haskell for Great Good!》

    {- 2017.02.21 <Haskell趣学指南 -- Learning You a Haskell for Great Good!> [官网](http://learnyouahas ...

  2. 《Haskell趣学指南》

    <Haskell趣学指南> 基本信息 原书名:Learn You a Haskell for Great Good!: A Beginner's Guide 原出版社: No Starch ...

  3. [2017.02.21-22] 《Haskell趣学指南 —— Learning You a Haskell for Great Good!》

    {- 2017.02.21-22 <Haskell趣学指南 -- Learning You a Haskell for Great Good!> 学习了Haskell的基本语法,并实现了一 ...

  4. Haskell 趣学指南 入门笔记(二)

    显示类型声明,Haskell是不用定义类型的原因,很像python 想要确定某个表达式的类型 *Main> :t 'a' 'a' :: Char *Main> :t True True : ...

  5. 《Haskell趣学指南 Learn You a Haskell for Great Good!》-代码实验

    doubleMe x = x + x doubleUs x y = doubleMe x + doubleMe y doubleSmallNumber x = then x else x * doub ...

  6. Haskell趣學指南--这个有意思

    正在慢慢了解不同于命令式的函数式语言. 希望能获得新的视野.. ~~~~~~~~~~~ http://learnyouahaskell-zh-tw.csie.org/zh-cn/ready-begin ...

  7. Ruby 趣学笔记(二)

    Ruby 趣学笔记(二) 本文写于 2020 年 5 月 7 日 类的继承 之前忘记写了,Ruby 的继承写法是: class IPhone < Phone def initialize(id, ...

  8. Ruby 趣学笔记(一)

    Ruby 趣学笔记(一) 本文写于 2020 年 5 月 6 日 Ruby 趣学笔记(一) 变量 变量声明 变量类型 常量 输出 字符串 字符串操作 Array 数组的遍历 数组的连接 怎么判断该变量 ...

  9. 自导自演的面试现场,趣学MySQL的10种文件

    导读 Hi,大家好!我是白日梦!本文是MySQL专题的第 24 篇. 今天我要跟你分享的MySQL话题是:"自导自演的数据库面试现场--谈谈MySQL的10种文件" 换一种写作风格 ...

随机推荐

  1. mongoDb +Java+springboot

    前言 :mongoDb 是一种比较常用的非关系数据库,文档数据库, 格式为json ,redis 有五种格式. 1. 项目中要使用,这里简单做个示例.首先是连接mongoDB,用的最多的robomon ...

  2. lucene4.0与之前版本的一些改变

    最近在用lucene4.0,因为之前也没用过lucene其它版本,所以也不是很熟悉.但每次上网查资料代码的时候,总发现网友们贴的代码都是之前的版本的.当我拷贝过来的时候总会出问题,去查API的时候,总 ...

  3. POJ - 1170 Shopping Offers (五维DP)

    题目大意:有一个人要买b件商品,给出每件商品的编号,价格和数量,恰逢商店打折.有s种打折方式.问怎么才干使买的价格达到最低 解题思路:最多仅仅有五种商品.且每件商品最多仅仅有5个,所以能够用5维dp来 ...

  4. 【转】 Android Studio SVN 使用方法

    Android Studio SVN 使用方法 如何安装配置SVN 请直接参考<SVN在Android Studio中的配置> http://www.cnblogs.com/songmen ...

  5. PermGen space错误解决方法

    在看下文之前,首先要确认意见事情,就是你是怎样启动tomcat的,我们在平时的开发环境其中,都是通过startup.bat方式启动tomcat的,那么你依照以下的方式,去改动/bin/catalina ...

  6. Qt 学习之路 :Qt 线程相关类

    希望上一章有关事件循环的内容还没有把你绕晕.本章将重新回到有关线程的相关内容上面来.在前面的章节我们了解了有关QThread类的简单使用.不过,Qt 提供的有关线程的类可不那么简单,否则的话我们也没必 ...

  7. ubuntu 配置android开发环境

    本文的下载地址都是androiddevtools,下载地址:http://www.androiddevtools.cn/ 一.安装android sdk 解压文件,全部放到/opt/Java/andr ...

  8. Android Studio 使用GitHub

    Android Studio 使用GitHub 1.安装配置 默认大家都已经安装了git软件,参考下图进行git与as关联 配置git  设置GitHub用户信息  填写完用户名,密码后可以点击Tes ...

  9. YII框架开发一个项目的通用目录结构

    YII框架开发一个项目的通用目录结构: 3 testdrive/ 4 index.php Web 应用入口脚本文件 5 assets/ 包含公开的资源文件 6 css/ 包含 CSS 文件 7 ima ...

  10. Java面向对象的概念以及OOP思想的优点

    传统面向过程程序设计的思路: 先设计一组函数用来解决一个问题,然后确定函数中需要处理的数据以及存储位置. 面向对象的设计的思路: 先确定处理的数据,然后确定处理数据的算法,最后将数据和算法封装在一起构 ...