只讲特性,不讲语法
惰性求值
1 | take 4 [13,26..] |
从步长为 13 的无限数组中取出前 4 位,可以出现无限数组,因为定义的时候并没有计算,而是调用的时候,前面写了 4 ,于是计算到第 44 个数时,就停止计算了
java 的 Stream 中的所有环节都是惰性的,本质只循环了一次
声明式编程
Haskell 中的 List Comprehension
如何取得所有三边长度皆为整数且小于等于 10,周长为 24 的直角三角形?
首先,把所有三遍长度小于等于 10 的三角形都列出来:
1 | ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ] |
上述代码从三个 List 中取值,并组合为一个三元组。接下来开始过滤这些三元组,增加限制条件:直角三角形,同时也考虑上 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] |
在命令式语言中,有控制流程,随着命令的执行,状态就会随之发生改变。
然而在函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”
元组
元组的使用表示二维向量,[[1,2],[8,1,15],[4,5]],尽管数组可可以表示,但是可能会产生三个数据,而元组能固定一个数据结构。
java 中有封装好的 Pair 和 Tuple,但是不能只是把它当作一个封装类去使用它,而要把它看作一种基本概念,就像枚举一样。
一切皆函数
Haskell 有一个中辍函数的概念
1 | -- 判断4是否在集合中 |
elem
就是一个中辍函数
1 | ghci> 1 + 1 |
加法本质上也是一个函数,通过 :t
可以查看它的类型约束,可以看到本质就是一个加法函数
模式匹配
1 | sayMe :: (Integral a) => a -> String |
sayMe 是一个函数,当调用 sayMe 2
时返回 “Two!”,当调用 sayMe 12
时返回 “Not between 1 and 5”,要是没有模式匹配的话,那可得好大一棵 if-else 树了!
1 | factorial :: (Integral a) => a -> a |
从上述代码可以看到,可以将模式匹配和递归配合起来,实现阶乘阶
门卫
1 | bmiTell :: (RealFloat a) => a -> a -> String |
通过 |
表示 if else,where 中定义的变量对所有门卫都可见,也可以在每个门卫内用 let 定义变量,只对门卫可见。
柯里函数
Haskell 中所有的函数都是柯里函数,只有一个参数
1 | ghci> max 4 5 |
max 函数的类型为 max :: (Ord a) => a -> a -> a
,也可以写作 max :: (Ord a) => a -> (a -> a)
,而不是 max :: (a, a) => a
柯里化和协程的区别:协程调用时需要一次性提供全部的入参,执行时可停顿,而柯里化不仅能实现执行的暂停,更重要的是函数可以方便的组合。
下面是一些示例:
1 | divideByTen :: (Floating a) => a -> a |
1 | -- 首字母是否大写 |
高阶函数
封装一个 zipWith 功能
1 | zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c] |
1 | ghci> zipWith' (+) [4,2,5,6] [2,6,2,3] |
一些其他的常用函数
map
1 | map :: (a -> b) -> [a] -> [b] |
1 | ghci> map (+3) [1,5,3,1,6] |
你可能会发现,以上的所有代码都可以用 List Comprehension 来替代。map (+3) [1,5,3,1,6]
与 [x+3 | x <- [1,5,3,1,6]
完全等价。
filter
1 | filter :: (a -> Bool) -> [a] -> [a] |
1 | ghci> filter (>3) [1,5,3,2,1,6,4,3,2,1] |
lambda
lambda 就是匿名函数。有些时候我们需要传给高阶函数一个函数,而这函数我们只会用这一次,这就弄个特定功能的 lambda。编写 lambda,就写个 \
1 | ghci> zipWith (\a b -> (a * 30 + 3) / b) [5,4,3,2,1] [1,2,3,4,5] |
由于有柯里化,如下的两段是等价的:
1 | addThree :: (Num a) => a -> a -> a -> a |
1 | addThree :: (Num a) => a -> a -> a -> a |
当然第一段代码更易读,不过第二个函数使得柯里化更容易理解。
有 $ 的函数调用
普通的函数调用符有最高的优先级,而$的优先级则最低。它可以减少我们代码中括号的数目。
sqrt 3 + 4 + 9
相当于 (sqrt 3) + 4 + 9
,但是如果想要获取 (3 + 4 + 9)
的平方根,可以写成 sqrt $ 3 + 4 + 9
相当于 sqrt (3 + 4 + 9)
除了减少括号外,$还可以将数据作为函数使用。
1 | ghci> map ($ 3) [(4+),(10*),(^2),sqrt] |
函数组合
在数学中,函数组合是这样定义的:(f*g)(x) = f(g(x))
haskell 中的函数组合与之很像,即 .
函数。其定义为:
1 | (.) :: (b -> c) -> (a -> b) -> a -> c |
假设我们有一组由数字组成的 list,要将其全部转为负数再求和,很容易就想到应先取其绝对值,再取负数再求和,像这样:
1 | ghci> map (\x -> sum (negate (abs x))) [5,-3,-6,7,-3,2,-19,24] |
.
函数写法为:
1 | ghci> map (sum . negate . abs) [5,-3,-6,7,-3,2,-19,24] |
不过含多个参数的函数该怎么办?我们可以使用不全调用使每个函数都只剩下一个参数
sum (replicate 5 (max 6.7 8.9))
可以重写为 (sum . replicate 5 . max 6.7) 8.9
或 sum . replicate 5 . max 6.7 $ 8.9
point free style
1 | max11' :: (Num a) => [a] -> a |
因为柯里化的存在,所以可以改写为:
1 | max11' :: (Num a) => [a] -> a |
同样,fn x = sum (replicate 5 (max 11 x))
可以改写为 fn = sum . replicate 5 . max 11
point free style会令你去思考函数的组合方式,而非数据的传递方式,更加简洁直白。不过函数若过于复杂,再使用point free style往往会适得其反,因此构造较长的函数组合链是不被鼓励的