函数式编程里的惰性求值

  1. Chapter: 声明式编程范式
    1. 1. 声明式编程范式初探
    2. 2. 再稍微深入理解声明式编程范式
    3. 3. 来开始一次函数式编程之旅
    4. 4. 阿隆佐·丘奇与λ演算系统
    5. 5. 初识怪异的函数式编程
    6. 6. 函数式编程有哪些优点?
    7. 7. 函数式编程里的高阶函数
    8. 8. 函数式编程里的currying柯里化
    9. 9. 函数式编程里的惰性求值
    10. 10. 函数式编程与Continuation/CPS
    11. 11. 函数式编程里的模式匹配
    12. 12. 函数式编程里的Closures闭包

一旦我们接纳了函数式哲学,惰性(或延迟)求值这一技术会变得非常有趣。在讨论并行时已经见过下面的代码片断:

1 String s1 = somewhatLongOperation1();
2 String s2 = somewhatLongOperation2();
3 String s3 = concatenate(s1, s2);

在一个命令式语言中求值顺序是确定的,因为每个函数都有可能会变更或依赖于外部状态,所以就必须有序的执行这些函数:首先是 somewhatLongOperation1,然后 somewhatLongOperation2,最后 concatenate,在函数式语言里就不尽然了。

前面提到只要确保没有函数修改或依赖于全局变量,somewhatLongOperation1 和 somewhatLongOperation2 可以被并行执行。

假设我们不想并行运行这两个函数,那是不是就按照字面顺序执行他们好了呢?答案是否定的,我们只在其他函数依赖于 s1 和 s2 时才需要执行这两个函数。我们甚至在 concatenate 调用之前都不必执行他们——可以把他们的求值延迟到 concatenate 函数内实际用到他们的位置。

如果用一个带有条件分支的函数替换 concatenate 并且只用了两个参数中的一个,另一个参数就永远没有必要被求值。在 Haskell 语言中,不确保一切都(完全)按顺序执行,因为 Haskell 只在必要时才会对其求值。

惰性求值优点众多,但缺点也不少。我们会在这里讨论它的优点而在下一节中解释其缺点。

优化

惰性求值有显著的优化潜力。惰性编译器看函数式代码就像数学家面对代数表达式——可以消去一部分而完全不去运行它,重新调整代码段以求更高的效率,甚至重整代码以降低出错,所有确定性优化(guaranteeing optimizations)不会破坏代码。这是严格用形式原语描述程序的巨大优势——代码固守着数学定律并可以数学的方式进行推理。

抽象控制结构

惰性求值提供了更高一级的抽象,它使得原本不可能的事情变成可能。例如,考虑实现如下的控制结构:

1 unless(stock.isEuropean()) {
2     sendToSEC(stock);
3 }

我们希望只在祖先不是欧洲人时才执行 sendToSEC。如何实现 unless?如果没有惰性求值,我们需要某种形式的宏(macro)系统,但 Haskell 这样的语言不需要它。把他实现为一个函数即可:

1 void unless(boolean condition, List code) {
2     if(!condition)
3         code;
4 }

注意如果条件为真,代码将不被执行。我们不能在一个严格(strict)的语言中再现这种求值,因为 unless 调用之前会先对参数进行求值。

无穷(infinite)数据结构

惰性求值允许定义无穷数据结构,对严格语言来说实现这个要复杂的多。

考虑一个 Fibonacci 数列,显然我们无法在有限的时间内计算出或在有限的内存里保存一个无穷列表。在严格语言如 Java 中,只能定义一个能返回 Fibonacci 数列中特定成员的 Fibonacci 函数,在 Haskell 中,我们对其进一步抽象并定义一个关于 Fibonacci 数的无穷列表,因为作为一个惰性的语言,只有列表中实际被用到的部分才会被求值。这使得可以抽象出很多问题并从一个更高的层次重新审视他们(例如,我们可以在一个无穷列表上使用表处理函数)。

缺点

当然从来不存在免费的午餐。惰性求值有很多的缺点,主要就在于…惰性。有很多现实世界的问题需要严格(按序)计算。例如考虑下例:

1 System.out.println(”Please enter your name: “);
2 System.in.readLine();

在惰性求值的语言里,不能保证第一行会在第二行之前执行!那么我们就不能进行输入输出操作,不能有意义地使用本地(native)接口(因为他们相互依赖其副作用必须被有序的调用),从而与整个世界隔离。如果引入允许特定执行顺序的原语又将失去数学地推理代码的诸多好处(为此将葬送函数式编程与其相关的所有优点)。

幸运的是,我们并非丧失了一切,数学家为此探索并开发出了许多技巧来保证在一定函数式设置下(functional setting)代码能以特定顺序执行。这样我们就赢得了两个世界。这些技术包括 continuation, monad 和 uniqueness typing(一致型别)。我只会在本文中解释 continuation,把 monad 和 uniqueness typing 留到将来的文章中。有趣的是,除了确保函数求值顺序, continuation 在很多别的情况下也很有用。这点等一会儿就会提到。

posted @ 2017-10-12 13:24  天涯海角路  阅读(883)  评论(0)    收藏  举报