Haskell入门篇九:高阶函数(中)

Posted Lambda小粽子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Haskell入门篇九:高阶函数(中)相关的知识,希望对你有一定的参考价值。

上一篇文章为大家介绍了高阶函数的定义,并用柯里化函数为切入点让大家对高阶函数有了一个初步的体会,这篇文章将继续上一篇的内容,为大家介绍高阶函数中非常重要的两个函数:左折叠函数foldl和右边折叠函数foldl

首先我们先来说一下什么是折叠函数和为什么我们要用折叠函数。折叠函数本身是定义在高阶函数内的,通过对可递归函数类型和被提供的操作符进行组合,递归的求出每一步的结果,并最终建立最终结果的一个过程。通过定义我们得知,折叠函数一般会被应用在一个可递归的数据类型上,如list 而折叠函数也分为左折叠和右折叠。在Haskell中,我们之前使用过的很多函数都可以用折叠函数进行重新定义。


我们先来说说右折叠函数foldr。在这里Foldable是一个类型类,所有应用了这个类型类的类型都是可被折叠的,具体的细节我们会在之后的文章中提到。让我们暂且仅仅聚焦在Haskell中的list上,foldr的类型可以被理解为 foldr :: (a -> b -> b) -> b -> [a] ->  blist也是Foldable的实例之一)。通过函数类型,我们可以看到foldr函数接收一个函数,一个element(通常而言是base element也就是这个函数的default value)和一个list作为变量。接下来我们尝试着自行定义下这个函数。

Haskell入门篇九:高阶函数(中)


通过定义我们发现函数会在listempty时达到basecase,而list不为空时依次对list中的element进行函数f的应用。这里一定要注意函数的类型(a ->  b -> b)。通过这个类型我们可以得知b为这个函数的basecase,而当我们处理list的时候,所有的element会被向右积累,即函数的展开应该为:

Haskell入门篇九:高阶函数(中)


所以通过对element的向右的积累,这个函数最后能够通过递归获得最终的结果。但是不知道大家有没有通过这个展开发现一些小小的问题,容我卖个关子,文章后面的部分会提到在某些情况下foldr并不是一个很好的选择。另外通过foldr,我们可以定义一些我们之前使用过的函数,这里我们以map函数为例。

Haskell入门篇九:高阶函数(中)


这其中(.)是我们下一篇文章会提到的复合函数。大家通过对myMap定义和myFoldr定义的对比不难看出,用foldr去定义map的时候,basecase就是[] emptylist。那么myFoldr的第一个argument的类型应为 a -> [b] -> [b],再加上我们观察得知中间的步骤应为先apply函数f在每一个element上,再将得到的结果放置在结果listhead上,所以我们可以写这么一个函数\a xs -> f a : xs,这个函数和上面那个函数的功能完全相同,通过化简我们可以得到\x -> (:)(f x)这个函数(之前的文章有提到过Haskell中的operator和函数的相互转换),而上面图中的形式就是这个函数通过复合得出的更为简洁的形式。 

接下来我们再来看看左结合函数foldl,同样的我们先来看看foldl的函数类型。

Haskell入门篇九:高阶函数(中)


我们暂且先关注于list上的foldl,那么foldl的类型应为foldl:: (b -> a -> b) -> b -> [a] -> b。不知大家有没有发现其类型和右折叠函数foldr的细微不同。我们先来尝试定义一下foldl

Haskell入门篇九:高阶函数(中)


也许大家现在发现不同了,相比于右折叠函数,左折叠函数在展开list的同时就对结果进行计算(而不是完全展开后再进行计算)。之前的文章中我有提到过一个概念『尾递归』,事实上左折叠函数就是一个尾递归函数,这种函数的好处是函数本身不会占用太多的function  stack函数栈,因为每一步的计算在函数展开的时候就已经得到结果,反之则需要将函数中的list(或者其他的递归函数类型)完全展开,在变量层级很多的情况下将展开非常多的function stack,有可能有stack overflow爆栈的危险。在这一类的尾递归函数中,以myFoldl为例,b通常被称为accumulator积累器。在后面的文章中我会详细介绍尾递归函数,在这里就不更多叙述了。在这里,我同样用一个例子来和大家一起深入的认识左结合函数的妙用,这里以reverse函数为例。


reverse函数是将一个list里面的element完全颠倒过来的函数,这个函数定义的难点是如我上面图中最后两行的定义,函数每次取出一个element都要递归的放到剩余listreverse结果之后的最后面,而之后的文章会提到的是,++函数将两个list相连本身的效率是非常的低的,所以在这种情况下我们选择使用accumlator,也就是我上面函数的第一个变量。我们传入一个emptylist,并每次递归将取出的结果concat到这个list上已达到reverse的效果。所以对比myReversemyFoldl的定义,我们知道这个函数中的base应该是[],那么foldl所接收的第一个函数的类型也就呼之欲出了 [a] -> a -> [a],所以我们可以将这个函数定义为(\xs x -> x : xs)。这个函数和(flip (:))函数是完全相同的,(flip (:))只是通过调换argument的顺序使得函数可以被η化简掉,这样的小tipsHaskell中有很多,我也会慢慢向大家介绍。

如何取舍使用foldr还是foldl,其实取决于我们的combine函数(或者是运算符)的结合方向,如果是左结合,我们就应该使用左折叠函数,反之应使用右折叠函数。上文的例子map函数中(:)操作符就是右结合的,所以我们用foldr去定义map函数。

下一篇文章将会介绍函数的复合,如何在Haskell中写出和数学函数表达式一样顺序的函数,敬请期待。



注:本文为原创内容,转载请标明出处。



以上是关于Haskell入门篇九:高阶函数(中)的主要内容,如果未能解决你的问题,请参考以下文章

Haskell入门篇八:高阶函数(上)

python入门16 递归函数 高阶函数

Python 高阶函数与函数式编程入门

07.Javascript——入门高阶函数

Golang入门到项目实战 | golang高阶函数

Android:Kotlin详细入门学习指南-高阶函数-基础语法