问题来自 Scala 函数式编程 一书的习题, 让我很困扰, 感觉函数式编程有点神学的感觉.后面看懂之后, 又觉得函数式编程所提供的高阶抽象是多么的强大. 这个问题让我发呆了好久, 现在把自己形成的想法分享下, 如果能少让一个人为这个问题烦恼, 那就再好不过了:)

问题: 如何通过函数 foldRight 实现 左折叠(left fold)操作 ?

这里只是讨论理论上的可行性, 实际中都是通过函数 foldLeft 实现右折叠操作,

因为函数 foldLeft 是栈安全的(不会因为压栈太深导致栈溢出).

答案:

def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B = {
foldRight(l, (b: B) => b)((a, g) => b => g(f(b, a)))(z)
}

那么下面我们来一步步解析这个答案:)

左折叠 与 右折叠

我们先看一下函数 foldRightfoldLeft 的定义:

def foldRight[A, B](l: List[A], b: B)(f: (A, B) => B):B = l match {
case Nil => z
case h::t => f(h, foldRight(t, b)(f))
} @annotation.tailrec
def foldLeft[A, B](as: List[A], z: B)(f: (B, A) => B):B = as match {
case Nil => z
case h::t => foldLeft(t, f(z, h))(f)
}

让我们看看一个列表L List(1, 2, 3) 左折叠和右折叠操作(foldRight) 的运行过程:

// 左折叠
f(3, f(2, f(1, b))) // 右折叠
f(1, f(2, f(3, b)))

从上面可以看到

  1. 左折叠操作, 初始值 b 先和列表最左边的元素进行 f 的运算, 从左往右依次计算.
  2. 右折叠操作, 初始值 b 先和列表最右边的元素进行 f 的运算, 从右往左依次计算.

再来看一个例子就更加清楚了

println(foldLeft(List(1,2,3), 0)((a, b) => { println(a, b); b + a}))
println(foldRight(List(1,2,3), 0)((a, b) => { println (a, b); b + a })) /* output
(1, 0)
(2, 1)
(3, 3)
6 (3, 0)
(2, 3)
(1, 5)
6
*/

复合函数

首先看看什么是复合函数? 下面是百度百科的定义

设函数y=f(u[x])的定义域为 Du,值域为 Mu,函数 u=g(x) 的定义域为 Dx,值域为 Mx,

如果 Mx∩Du≠Ø,那么对于 Mx∩Du 内的任意一个x经过u;有唯一确定的y值与之对应,则变量x与y之间通过变量u形成的一种函数关系,这种函数称为复合函数(composite function),记为:y=f[g(x)],其中x称为自变量,u为中间变量,y为因变量(即函数)。

不是任何两个函数都可以复合成一个复合函数,只有当Mx∩Du≠Ø时,二者才可以构成一个复合函数。

让我们创建两个函数:

scala> def f(s: String) = "f(" + s + ")"
f: (String)java.lang.String scala> def g(s: String) = "g(" + s + ")"
g: (String)java.lang.String

compose

compose 组合其他函数形成一个新的函数 f(g(x))

scala> val fComposeG = f _ compose g _
fComposeG: (String) => java.lang.String = <function> scala> fComposeG("hello")
res0: java.lang.String = f(g(hello))

andThen

andThen 和 compose很像,但是调用顺序是先调用第一个函数,然后调用第二个,即g(f(x))

scala> val fAndThenG = f _ andThen g _
fAndThenG: (String) => java.lang.String = <function> scala> fAndThenG("hello")
res1: java.lang.String = g(f(hello))

关键一步

这里把解决方法再贴出来, 方便对照

def foldLeftViaFoldRight[A,B](l: List[A], z: B)(f: (B,A) => B): B =
foldRight(l, (b:B) => b)((a, g) => b => g(f(b, a)))(z)

将 匿名函数 ((a, g) => b => g(f(b, a))) 改写为

def h(a: A, g: B => B): (B => B) =
g compose ((x: B) => f(x, a));

怎么想?

通过上面函数的定义, 我们可以把解决方法写成

def foldLeftViaFoldRight_2[A,B](l: List[A], z: B)(f: (B,A) => B): B = {
def h(a: A, g: B => B): (B => B) = g compose ((x: B) => f(x, a))
def identity(b: B) = b // 恒等函数 foldRight(l, identity _)(h _)(z)
}

我们想通过函数 foldRight 实现 foldLeft, 而foldRight 从右边处理列表元素, 而我们想从左边处理列表元素.

以列表 List(1, 2, 3)为例,

由 右折叠的定义, 计算过程为:

// 右折叠
h(1, h(2, h(3, identity)))

而我们想要实现左折叠操作, 即函数的最终运行结果应该是

f(f(f(1, z), 2), 3)

这里的技巧就在于函数 g 的类型为 B => B, 函数 h 类型也是 B => B,函数 identity 的类型为 B => B; 这样就能进行组成一条符合函数链;

然后我们通过复合函数链完成上述的思路.

有一点在这很重要, 那就是, g 和 h 都是高阶抽象函数, 你可能很想知道函数 g 的具体实现是什么, 其实这是不必要的也是错误的想法;

我们不要在意 g 具体实现是什么, 那么我们要关注什么呢?

那就是函数 g, h, identity 类型为 B => B, 一个例子就能让你清楚的明白上面再说什么了, 你也能看出来函数 g 其实并不是一个具体的函数, 它随着计算阶段也在不停的变动, 这就是理解的难点所在

例子

现在让我们看一下当我们传递列表 List(1, 2)时, 发生了什么:


val l = List(1, 2) foldRight(l, identity[B] _)(h _) = h(1, h(2, identity([B]))) // 由 右折叠 的定义可知 = h(1, identity[B] compose ((x: B) => f(x, 2))) // 展开里面的 `h` // 由于 identity[B] 是恒等函数,
// identity[B] compose ((x: B) => f(x, 2)) =
// identity(((x: B) => f(x, 2))) = ((x: B) => f(x, 2))
= h(1, ((x: B) => f(x, 2))) // 展开另一个 'h'
= ((x: B) => f(x, 2)) compose ((x: B) => f(x, 1)) // 由 function composition 定义可知 f1 _ compose f2 = f1(f2)
= (x: B) => f(f(x, 1), 2) // 左折叠操作

看看上面的过程, 我们实现了转换:

h(1, h(2, identity([B]))) => f(f(x, 1), 2)

即通过右折叠函数 foldRight 实现了左折叠操作

这里函数 g 先后是,

  • identity([B])
  • ((x: B) => f(x, 2))

正是函数类型为 "B => B" 进行复合的技巧所在

并且我们看到, 对于列表的每一元素 a, 都会建立一个 包含函数 g 的函数 h, 并且每个新的函数h的都是前一个函数 h 的输入。

一点思考

1. 关于函数 identity 是如何得来的 ?

如果列表 l 是空列表, 那么 foldLeftViaFoldRight 应该返回 z,

所以函数 h 应该是一个恒等函数, 所以我们将恒等函数作为第二个参数传递给 foldRight.

2. 这里通过建立一条复合函数链, 通过利用函数压栈中, 栈的先进后出特性实现了计算顺序的翻转; 所以方法是非栈安全的, 数据太大有栈溢出的风险, 这里只是说明理论的可行xing

函数式编程之foldLeftViaFoldRight的更多相关文章

  1. Python函数式编程之map()

    Python函数式编程之map() Python中map().filter().reduce()这三个都是应用于序列的内置函数. 格式: map(func, seq1[, seq2,…]) 第一个参数 ...

  2. python3 第二十一章 - 函数式编程之return函数和闭包

    我们来实现一个可变参数的求和.通常情况下,求和的函数是这样定义的: def calc_sum(*args): ax = 0 for n in args: ax = ax + n return ax 但 ...

  3. python3 第二十章 - 函数式编程之Higher-order function(高阶函数)

    什么是高阶函数?把函数作为参数传入或把函数做为结果值返回,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式.函数式编程的特点: 函数本身可以赋值给变量,赋值后变量为函数: 允许将函数本身 ...

  4. Python函数式编程之lambda表达式

    一:匿名函数的定义 lambda parameter_list: expression 二:三元表达式 条件为真时返回的结果 if 条件判断 else 条件为假的时候返回的结果 三:map map(f ...

  5. 函数式编程之-bind函数

    Bind函数 Bind函数在函数式编程中是如此重要,以至于函数式编程语言会为bind函数设计语法糖.另一个角度Bind函数非常难以理解,几乎很少有人能通过简单的描述说明白bind函数的由来及原理. 这 ...

  6. 函数式编程之-F#类型系统

    在深入到函数式编程思想之前,了解函数式独有的类型是非常有必要的.函数式类型跟OO语言中的数据结构截然不同,这也导致使用函数式编程语言来解决问题的思路跟OO的思路有明显的区别. 什么是类型?类型在编程语 ...

  7. 函数式编程之-Partial application

    上一篇关于Currying的介绍,我们提到F#是如何做Currying变换的: let addWithThreeParameters x y z = x + y + z let intermediat ...

  8. 函数式编程之-Currying

    这个系列涉及到了F#这门语言,也许有的人觉得这样的语言遥不可及,的确我几乎花了2-3年的时间去了解他:也许有人觉得学习这样的冷门语言没有必要,我也赞同,那么我为什么要花时间去学习呢?作为一门在Tiob ...

  9. java函数式编程之Supplier

    原创 2016年12月25日 10:02:52 标签: 函数式编程 / java 3250 描述:Supplier< T>接口没有入参,返回一个T类型的对象,类似工厂方法. 源码: pub ...

随机推荐

  1. 杜教筛:Bzoj3944: sum

    题意 求\(\sum_{i=1}^{n}\varphi(i)和\sum_{i=1}^{n}\mu(i)\) \(n <= 2^{31}-1\) 不会做啊... 只会线性筛,显然不能线性筛 这个时 ...

  2. 云计算之路-阿里云上:服务器CPU 100%问题是memcached的连接数限制引起的

    非常抱歉,昨天的服务器CPU 100%问题是达到 memcached 的连接数限制引起的,不是阿里云服务器的问题. 之前我们用的是阿里云“云数据库 memcached 版”,上个周末我们换成了自己搭建 ...

  3. Android OpenGL ES 开发(九): OpenGL ES 纹理贴图

    一.概念 一般说来,纹理是表示物体表面的一幅或几幅二维图形,也称纹理贴图(texture).当把纹理按照特定的方式映射到物体表面上的时候,能使物体看上去更加真实.当前流行的图形系统中,纹理绘制已经成为 ...

  4. 使用CMD命令编译和运行Java程序

    对于初学者来说,使用CMD命令(Unix以及类Unix系统采用Termial)来编译和运行Java的好处是让初学者直观地体会到编译(Compile)这一步骤,加深记忆.所谓编译就是将文本文件xxx.j ...

  5. Node与apidoc的邂逅——NodeJS Restful 的API文档生成

    作为后台根据需求文档开发完成接口后,交付给前台(angular vue等)做开发,不可能让前台每个接口调用都去查看你的后台代码一点点查找.前台开发若不懂你的代码呢?让他一个接口一个接口去问你怎么调用, ...

  6. 免费后台管理UI界面、html源码推荐

    一个好的UI应该满足的条件应该达到如下几个: 1.美观.大方.简洁 2.兼容IE8.不考虑兼容IE6/IE7,因为现在还有很多公司在使用Win7系统,系统内置了IE8 3.能通过选项卡打开多个页面,不 ...

  7. 【Unity3D与23种设计模式】中介者模式(Mediator)

    GoF中定义: 定义一个接口来封装一群对象的互动行为 中介者通过移除对象之间的引用 以减少他们之间的耦合度 并且能改变它们之间的互动独立性 游戏做的越大,系统划分的也就越多 如事件系统,关卡系统,信息 ...

  8. phpstorm激活码生成器地址

    http://idea.qinxi1992.cn/ 写在windows/system32/drivers/etc/hosts里 0.0.0.0 account.jetbrains.com 激活码位置: ...

  9. 前端的UI设计与交互之数据录入篇

    数据录入是获取对象信息的重要交互方式,用户会频繁的增加.修改或删除信息.多种多样的文本录入和选择录入方式帮助用户更加清晰和高效的完成这项体验.设计者应当注意这几点:为初级用户/偶尔访问的用户提供简单易 ...

  10. 打印机驱动冲突和端口异常:win10更新部分补丁后,打印机本地连接(连接打印机的主机)可以打印,其他共享网络中的电脑可以连接到打印机,但不能打印——解决方案

    一.问题描述: 1.A(WIN10系统)表示连接打印机的电脑,P表示打印机(型号:惠普127M),B(WIN7系统)表示局域网中的电脑 2.A升级后部分补丁后,A可以使用打印机P打印文件,B显示可以连 ...