承上文
在慢慢学Haskell(一):Hello Haskell结尾我提到了Haskell官网上遍历素数的示例
primes = filterPrime [2..]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x `mod` p /= 0]
本文中我们将逐一了解其中涉及到的概念。
List
Haskell中的List是一个单类型的数据结构,也就是说一个List中只能存储一种类型的数据。不能像JS中数组那样存储不同类型的数据。
let demoList = [1,2,3,4] -- √ correct
let wrongDemoList = [1,2,3,'4'] -- × wrong, raise an error
:运算子
:运算子可以把一个元素和一个List结合,比如1:[2,3,4]的执行结果是[1,2,3,4],而[1,2,3,4]可以理解为1:2:3:4:[]的语法糖。
Prelude> "Hello" : ["Lilei"]
["Hello","Lilei"]
Prelude> 1:[2,3,4]
[1,2,3,4]
在处理函数参数的时候,如果参数是一个List,我们可以用x:xs的模式,来表示参数List中的第一个元素和剩余部分,如示例中的filterPrime (p:xs),如果执行filterPrime [1,2,3,4],那么p对应1,xs对应[2,3,4]
在第一次接触x:xs的时候让我觉得和js中解构赋值的语法有相似之处。
function getArgs([x, ...xs]){
console.log('x = ', x)
console.log('xs =', xs)
}
/**
* getArgs([1,2,3,4]) =>
* x = 1
* xs = [2,3,4]
*/
Range
我想每个人都有在纸上手写一个数字列表的经历。当数字列表很长的时候,很多人都会简写成类似1,2,3...10的形式,这非常符合直觉。而Range就是这样一个符合直觉的List构造方法。通过Range我们不仅可以用[1..10]来表示[1,2,3,4,5,6,7,8,9,10],甚至还可以用['A'..'Z']这样的方式来表示全部的大写字母。
Prelude> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
Prelude> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"
Haskell甚至还支持自动推导Range的步长。比如我们可以这样表示0到50之间的全部偶数:
Prelude> [0,2..50]
[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50]
由于Haskell惰性求值的特点,诸如[2..]这种无限长度的List也是可以声明的。但是Hakell不会在声明之时计算整个列表的数据,而是在List参与运算时根据运算所需进行求值。
Prelude> take 10 [2..]
[2,3,4,5,6,7,8,9,10,11]
我们不妨把这个无限长度的列表理解成一个初始数据加上演变规则
注:take是List相关的一个函数,用于获取List的前n项
List Comprehension
List Comprehension在《Haskell函数式编程》一书中被翻译成列表生成器,也有人称之为列表推导式。不管翻译成什么名字,List Comprehension本质上一个列表+规则=新列表的列表构造方式,与数学中基于初始集合定义新集合的方式不能说毫无关系,只能说是一模一样。
上面公式表示,10以内正整数的平方数的集合,在Haskell中可以这样写:
Prelude> [x^2 | x <- [1..100], x < 10]
[1,4,9,16,25,36,49,64,81]
x<-[1..100]代表原列表。- 逗号后面的
x < 10代表对列表数据的过滤条件,过滤条件可以有多个,用逗号间隔。 |左侧的x^2是输出函数,用于处理过滤后的列表数据
多过滤条件的情形
Prelude> [x^2 | x <- [1..100], x < 10, x > 4]
[25,36,49,64,81]
通过List Comprehension过滤字符串中的大写字母
Prelude> filterUpper st = [x| x <- st, x `elem` ['A'..'Z']]
Prelude> filterUpper "Hello World"
"HW"
Where关键字
where关键字用于定义只在当前函数内可见的函数,这样说有一点绕口,让我们看个例子:
sayHiToOthers xs = [sayHi x | x <- xs]
where sayHi x = "Hi " ++ x
在通过where关键字我们定义了只能在sayHiToOthers中使用的函数sayHi,并通过sayHi函数构建了一个新的List
Prelude> sayHi ["LiLei", "HanMeimei"]
["Hi LiLei","Hi HanMeimei"]
埃拉托斯特尼筛法
埃拉托斯特尼筛法是找出一定范围内所有素数最有效的方法之一,其命名源自古希腊数学家埃拉托斯特尼。引用一下百度百科中对其算式的描述。
要得到自然数n以内的全部素数,必须把不大于
的所有素数的倍数剔除,剩下的就是素数。
给出要筛数值的范围n,找出以内的素数。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个质数,也就是3筛,把3留下,把3的倍数剔除掉;接下去用下一个质数5筛,把5留下,把5的倍数剔除掉;不断重复下去......。
总结一下
primes = filterPrime [2..]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x `mod` p /= 0]
结合上述内容我们再来看下文章开头的示例。
- 通过
where关键字,我们为primes定义个了一个名为filterPrime的函数 filterPrime函数接受一个List作为参数,计算结果由List的首个元素p,与filterPrime递归调用自身的结果结合而成。- 参照埃拉托斯特尼筛法,
filterPrime递归调用自身时,所传入的参数为初始List排除首个元素P之后的剩余部分xs,排除其中可以被p整除的元素后得到的List。 - 由于[2..]是一个无限长度的List,那么很明显
filterPrime的递归调用没有终点,因而我们得到的primes是一个无限长度的List。
探究下filterPrime的执行过程
-- 第一步,得到 2 : xs, xs代表大于2,不能被2整除的正数
-- 第二步,得到 2 : 3 : xss, xss代表大于3且不能被2,3整除的正数
-- 第三步,得到 2 : 3 : 5 : xsss, xsss代表大于5且不能被2,3,4整除的正数
-- ...
至此我们完成了埃拉托斯特尼筛法筛选素数的Haskell实现。
Next
与JS不同,Haskell是一门强类型语言,Haskell中的类型系统非常重要,被称为学习Haskell的第一座大山,接下来就让我们初探Haskell的类型系统。