Haskell带你玩转函数

函数,再简单不过的概念。不管是什么范式的编程语言,都离不开用函数来表达逻辑。而函数式编程,当然核心就是函数。但函数式编程中的函数与其他语言有什么区别呢?本节都带着大家领略一下Haskell中强大的函数。这也是我们最应该从Haskell中学习,如果你觉得Haskell或其他函数式语言中的其他概念都太复杂太学术,那函数就是你至少应该从中收益的。再回到日常项目的非函数式语言时,学会领悟如何识别概念、分析数据流,然后设计一个简洁而通用的函数,并在此之上构建起更复杂的抽象。而不是一股脑地传入10个参数,然后在函数内写上一千行的逻辑,再交给别人去维护。


1.理论基础

1.1 Partially Applied

一个令初学者难以置信的事实是:Haskell中的函数都只接受一个入参。那为什么我们可以定义任意个参数的函数呢?答案就是Curry化。所谓Curried函数就是指:不直接接受多个参数,而是每次接受一个入参,同时返回一个函数接受剩下的参数。返回的函数也叫Partially Applied函数。在Python中我们也能看到类似的概念,即itertools.partial()函数。传入一个函数和部分参数得到一个新函数,用起来很方便,正确使用的话能够增加函数的重用度,减少重复代码。

Prelude> let mul x y = x * y
Prelude> let mul2 = mul 2
Prelude> mul2 3
6
Prelude> mul2 5
10

同样的,因为Curry化,函数的定义可以更加简洁,比如下面两种写法是等价的。

Prelude> foo a = bar b a
Prelude> foo = bar b

1.2 一次性函数:Lambda

大家应该很熟悉用Lambda表达式定义匿名函数,在主流的Python、Java里都有支持,甚至在Java 8之前我们已经有用匿名内部类的习惯,尤其是Swing图形化编程。Haskell中Lambda也一样,但更强大的一点是Haskell的Lambda也像普通函数一样支持模式匹配,但是你只能定义一个模式。如果不匹配,则会抛出运行时异常。

Prelude> map (\(a,b) -> a + b) [(1,2),(3,5),(2,6)]
[3,8,8]

2.玩转函数

通过Curry化,我们能够产生大量可用的函数,这在其他非函数式编程语言里是无法想象的。有了这样的可能性,在开发新函数时我们就会特别注意,让自己的函数更加通用、可重用。下面就来看看有了Curry,我们都可以做什么。

2.1 Section

因为Haskell还支持Infix中缀函数,Infix函数也可以通过Section变成Partially Applied函数。什么是Section呢?就是只提供任意一侧的参数,并用括号括起来得到一个新的函数。

Prelude> let add2 = (2+)
Prelude> add2 3
5
Prelude> add2 5
7
Prelude> let divBy2 = (/2) 
Prelude> divBy2 10
5.0
Prelude> divBy2 20
10.0

2.2 Application

Haskell中提供了一个infix运算符f x = f x。这不是废话么?但$存在是尤其道理的,一就是它是右结合的,所以链式函数调用时可以省掉恼人的括号(还记得Lisp中最后末尾那经常被人嘲笑的一大串反括号么)。下面的例子不可避免地引入了后面要讲的map,如果不熟悉可以先跳过再回来看这部分。

Prelude> sum (filter (> 10) (map (*2) [2..10]))
80 
Prelude> sum $ filter (> 10) $ map (*2) [2..10]
80

二就是当第一个参数f的实参函数不确定时,它可以将第二个参数值变成函数,这有什么用呢?说起来有些拗口,还是看下面的例子吧。因为map只接受函数,所以(*2)是没问题的,可单独传入3就不行了。这时就要$来帮忙了,将3变成函数,等待第一个参数f的到来。

Prelude> map (*2) [2..10]
[4,6,8,10,12,14,16,18,20]

Prelude> map 2 [(4+), (*10), (^2), sqrt]
<interactive>:20:1:
    Non type-variable argument in the constraint: Num ((a -> a) -> b)
    (Use FlexibleContexts to permit this)
    When checking thatithas the inferred type
      it :: forall b a. (Floating a, Num ((a -> a) -> b)) => [b] 

Prelude> map ($ 2) [(4+), (*10), (^2), sqrt]
[6.0,20.0,4.0,1.4142135623730951]

2.3 Composition

在数学上,我们可以组合两个函数,比如h(x)=g(f(x))。在Haskell中我们同样可以这样做,答案就是就是这个奇妙的点“.”。它的定义是f . g = \x -> f (g x)。它也有两个优点,一就是随时随地组合出新函数。

Prelude> map (\x -> negate (abs x)) [5,-3,-6,7,-3]
[-5,-3,-6,-7,-3]

Prelude> map (negate . abs) [5,-3,-6,7,-3]
[-5,-3,-6,-7,-3]

Prelude> negate . sum . tail $ [1..5]
-14

二就是简化函数定义。因为当函数嵌套时,我们没法像前面foo和bar的例子那样,省略掉参数。

Prelude> let fn x = ceiling (negate (tan (cos (max 50 x))))

Prelude> let fn = ceiling . negate . tan . cos . max 50

3.高阶函数:MapReduce抽象

3.1 Map映射

从LISP到Haskell,甚至到主流的Python,我们都能看到map-filter-reduce的影子。没错,这就是分布式计算的MapReduce思想的源头。编程时,我们大部分时间都在操纵数组或链表等集合,比如取出每个元素里的一个属性值替换它自己、排除掉某些元素保留剩下的、求和求平均值等等。而我们的这些操作可以抽象出更高的层次,即map(映射)、filter(过滤)、reduce(化简)。前面介绍的Partially Applied和Lambda匿名函数至此才算是发挥出真正的威力!

map很简单,List comprehension也能达到同样效果。Python比较推崇后者,具体还要看在特定情况下,哪种的代码更简洁。除了map的基本用法,你还可以像下面这样尽情发挥想象,写出优雅的代码。比如我们还可以嵌套map,像手术刀一般切入到List的List中,处理每一个元素。

Prelude> map (+3) [1,3,5,7,9]
[4,6,8,10,12]
Prelude> [x+3 | x <- [1,3,5,7,9]]
[4,6,8,10,12]

Prelude> map (map (^2)) [[1,2],[3,4,5],[6]]
[[1,4],[9,16,25],[36]]

3.2 Filter过滤

类似地,filter的用法也同样很直观。

Prelude> filter (>3) [1,5,3,2,1,6,4,3,2,1]
[5,6,4]

3.3 Fold化简

Haskell里的reduce叫做fold,它接受一个二元函数和初始值。根据遍历的方向分成foldl和foldr,如果想将列表的第一元素作为初始值的话就用foldl1和foldr1。

Prelude> let sum' = foldl1 (\acc x -> acc + x)
Prelude> sum' [3,5,2,1]
11

总结一下,map-filter-fold都相当于:用递归实现的循环,外加一个指定函数作为循环体,实现映射、过滤、化简的功能,可以说是强迫我们写出简洁、单一职责的代码。其中,map接受一元函数,返回新元素。filter也接受一元函数,但返回布尔值决定是否保留原来的元素。而fold接受二元函数,返回汇总的值。

Prelude> let sum = foldl (+) 0
Prelude> sum (filter (> 10) (map (*2) [2..10]))
80
Prelude> sum $ filter (> 10) $ map (*2) [2..10]
80

4.初露端倪:Lazy的威力

这里具体说一下为什么要有foldr,以下面的[3,5,2,1]为例,foldl的效果就是f (f (f (f z 3) 4) 5) 6,其中z是初始值。如果我们处理的是无穷列表的话,则会陷入死循环,因为我们永远也到达不了最后一个元素,也就永远无法开始真正的处理。而foldr的执行过程则是:f 3 (f 4 (f 5 (f 1 z))),如果二元函数有时可以通过第一个参数就确定结果的话(比如逻辑判断,Java等很多语言里都实现了“短路”效果),foldr就能帮助我们处理无穷列表。

Prelude> foldr (&&) True (repeat False)
False

因为Haskell很懒,所以如果第一个参数已经是False的话,就不会去继续展开(递归调用)列表后面的元素了。

True && x = x
Flase && _ = Flase

这里的例子可能在其他语言中也能实现,之后我们会逐渐看到Lazy Evaluation的强大威力。

展开阅读全文
©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值