本节书摘来自异步社区《Haskell趣学指南》一书中的第1章,第1.5节我是列表推导式,作者 【斯洛文尼亚】Miran Lipovaca,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.5 我是列表推导式列表推导式(list comprehension)是一种过滤、转换或者组合列表的方法。
学过数学的你对集合推导式(set comprehension)概念一定不会陌生。通过它,可以从既有的集合中按照规则产生一个新集合。前10个偶数的集合推导式可以写为{2 · x | x∈N, x≤ 10},先不管语法,它的含义十分直观:“取所有小于等于10的自然数,各自乘以2,将所得的结果组成一个新的集合。”
若要在Haskell中实现上述表达式,我们可以通过类似take 10 [2,4..]的代码来实现。不过,使用列表推导式也是同样的轻而易举:
ghci> [x*2 | x <- [1..10]] [2,4,6,8,10,12,14,16,18,20]我们可以再仔细看看这段代码,理解一下列表推导式的语法。
在[x*2 | x <- [1..10]]这段代码中,我们通过[x <- [1..10]]取了[1..10]这个列表中的每一项元素,x即[1..10]中的每一项元素的值,也可以说,x是[1..10]中每一项元素的绑定。竖线(|)前面的部分指列表推导式的输出,表示所取的值与计算结果的映射关系。在这个例子里,我们就是取[1..10]中的所有数字的2倍了。
看起来,这要比第一个例子长得多,也复杂得多。但是让所有数字乘以2只是一种简单的情景,如果遇到更复杂的情况该怎么办?这才是列表推导式大显身手的地方。
比如,我们想给这个列表推导式再添一条谓词(predicate)。它位于列表推导式最后面,与前面的部分由一个逗号分隔。在这里,我们只取乘以2后大于等于12的元素。
ghci> [x*2 | x <- [1..10], x*2 >= 12] [12,14,16,18,20]若是取50~100中所有除7的余数为3的元素该怎么办?很简单:
ghci> [ x | x <- [50..100], x `mod` 7 == 3] [52,59,66,73,80,87,94]注意:从一个列表中筛选出符合特定谓词的元素的操作,也称为过滤(filter)。再举个例子,假如我们想要一个列表推导式,它能够使列表中所有大于10的奇数变为"BANG",小于10的奇数变为"BOOM",其他则统统扔掉。方便起见,我们将这个推导式置于一个函数之中,使它易于重用。
boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]注意:记住,如果是在GHCi中定义这个函数,必须在函数名前面放一个let关键字。不过将函数写在脚本里再装载到GHCi的话就不必再加let了。odd函数判断一个数是否为奇数:如果是,返回True;否则返回False。某项元素只有满足所有谓词时,才会被列表推导式留下。
ghci> boomBangs [7..13] ["BOOM!","BOOM!","BANG!","BANG!"]也可以加多个谓词,中间用逗号隔开。比如,取10~20中所有不等于13、15或19的数,可以这样:
ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19] [10,11,12,14,16,17,18,20]除了多项谓词之外,从多个列表中取元素也是可以的。当分别从多个列表中取元素时,将得到这些列表中元素的所有组合:
ghci> [x+y | x <- [1,2,3], y <- [10,100,1000]] [11,101,1001,12,102,1002,13,103,1003]这里的x取自[1, 2, 3],y取自[10, 100, 1000]。随后这两个列表按照如下的方式组合:首先x成为1,同时y分别取[10, 100, 1000]中的每一个值。由于列表推导式的输出为x+y,可得到11、101、1001作为结果的开头部分(即1分别与10、100与1000相加的结果)。随后x成为2,同理可得12、102与1002,追加到结果的后面。对3也同理。
按照这一规律,[1,2,3]中的每个元素都与[10,100,1000]中的元素按照所有可能的方式相组合,再按x+y取得所有这些组合的和。
下面是另一个例子。假设有两个列表,即[2,5,10]和[8,10,11],要取它们所有组合的积,可以写出这样的列表推导式:
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]] [16,20,22,40,50,55,80,100,110]意料之中,得到的新列表长度为9。若只取乘积大于50的结果该怎样?只需要再加一条谓词:
ghci> [ x*y | x <-[2,5,10], y <- [8,10,11], x*y > 50] [55,80,100,110]我们再取一个包含一组名词和形容词的列表推导式吧,写诗的话也许用得着。
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,随后通过sum将它们加起来,从而得到列表长度。
length' xs = sum [1 | _ <- xs]这里我们用了一个下划线(_)作为绑定列表中元素的临时变量,表示我们并不关心它具体的值。
记住,字符串也是列表,完全可以使用列表推导式来处理字符串。下面是一个除去字符串中所有非大写字母的函数:
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]在这里,主要的工作由谓词完成。它给出了保证:只有位于['A'..'Z']这一区间之内的字符才被视为是大写。我们可以将这个函数装载到GHCi测试一下:
ghci> removeNonUppercase "Hahaha! Ahahaha!" "HA" ghci> removeNonUppercase "IdontLIKEFROGS" "ILIKEFROGS"对于列表的列表可以通过嵌套的列表推导式处理。假设有一个包含许多数值的列表的列表,让我们在不拆开它的前提下除去其中的所有奇数:
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]]这里的输出部分嵌套了另一个列表推导式。已知列表推导式的返回结果永远是列表,因而可以看出,这里的返回结果是一个由数值组成的列表的列表。
注意:为提升可读性,将列表推导式分成多行也是可以的。若非在GHCi之下,还是将列表推导式分成多行比较好,尤其是需要嵌套的时候。
相关资源:七夕情人节表白HTML源码(两款)