• 函数式编程之foldLeftViaFoldRight


    问题来自 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

  • 相关阅读:
    Python 面向对象 —— super 的使用(Python 2.x vs Python 3.x)
    安全移除驱动器、弹出、卸载的差别及详细查看设备的运行前后的异同
    java中不常见的keyword:strictfp,transient
    textarea文本域宽度和高度(width、height)自己主动适应变化处理
    Android 输入框弹出样式
    .net下载优酷1080P视频
    Oracle Hints具体解释
    关于成本核算方法、步骤、成本分析的简单回复
    程序猿接私活经验总结,来自csdn论坛语录
    Android getResources的作用和须要注意点
  • 原文地址:https://www.cnblogs.com/nowgood/p/foldLeftViaFoldRight.html
Copyright © 2020-2023  润新知