【问题标题】:How to not fall into R's 'lazy evaluation trap'如何不落入 R 的“懒惰评估陷阱”
【发布时间】:2015-05-19 00:03:12
【问题描述】:

“R 传递 promises,而不是值。承诺在第一次评估时被强制执行,而不是在传递时。”,参见 G. Grothendieck 的 this answer。另请参阅 this question 指的是 Hadley 的书。

在简单的例子中,例如

> funs <- lapply(1:10, function(i) function() print(i))
> funs[[1]]()
[1] 10
> funs[[2]]()
[1] 10

可以考虑这种不直观的行为。

但是,我发现自己在日常开发过程中经常陷入这个陷阱。我遵循一种相当函数式的编程风格,这意味着我经常有一个函数 A 返回一个函数 B,其中 B 在某种程度上取决于调用 A 的参数。依赖关系不像上面的例子那么容易看出,因为计算复杂并且有多个参数。

忽略这样的问题会导致难以调试的问题,因为所有计算都运行顺利 - 除了结果不正确。只有对结果的明确验证才能揭示问题。

最重要的是,即使我注意到了这样的问题,我也永远无法确定 force 需要哪些变量,哪些不需要。

我怎样才能确保不落入这个陷阱?是否有任何编程模式可以防止这种情况发生,或者至少确保我注意到存在问题?

【问题讨论】:

  • 似乎这可能是一个重新评估您的编程风格的好机会,如果您强迫您正在使用的语言做一些它本来就没有设置做的事情......跨度>
  • ...这就是为什么我要寻找能够规避这个问题的模式。 ;-)
  • 这个问题仍然具有历史意义,但今天的读者可能有兴趣知道,从 R 3.4.1(可能早在 3.2.0)开始,OPs 帖子中的示例引起了乐趣[[5 ]]() 生成5
  • @russellpierce 确实如此(并且确实在 R 3.2.0 中进行了更改),但它并没有使这个问题过时。唯一改变的(据我所知)是基础 R 中的高阶函数,例如 lapplyReduce 现在自动 force 它们的参数。惰性求值原则仍然存在,因此这不会改变自定义函数的任何内容。根据下面提供的答案,正确的解决方案似乎是在自定义高阶函数中始终使用 force 函数参数,或者使用提供的机制,例如 lapplyCurry
  • @jim 感谢您的上下文。那么,今天该问题的重现会是什么样子?

标签: r design-patterns functional-programming lazy-evaluation


【解决方案1】:

R 有一个函数可以帮助防止惰性求值,例如在创建闭包的情况下:forceAndCall()

来自在线 R 帮助文档:

forceAndCall 旨在帮助定义高阶函数(如 apply),以便在应用函数返回的结果是捕获其参数的闭包时表现得更合理。

【讨论】:

    【解决方案2】:

    在处理此类情况时,正在改进 R 的高阶函数,如应用函数、Reduce 等。这是否会导致 R 3.2.0 在几周内发布取决于这些变化的破坏性程度。应该会在一周左右变得清晰。

    【讨论】:

    • 一旦有更多信息可用,是否 +1 以鼓励修改。 ;)
    • R 3.2.0 中确实包含了一些相关的更改。我想这就是你所指的一切?还是计划进行更多更改?
    【解决方案3】:

    正如其他人指出的那样,这可能不是 R 中最好的编程风格。但是,一个简单的选择是养成强制一切的习惯。如果您这样做,请意识到您实际上不需要调用force,只需评估符号即可。为了让它不那么难看,你可以把这样的函数作为一种习惯:

    myfun<-function(x,y,z){
       x;y;z;
       ## code
    }
    

    【讨论】:

    • force 的冗长或“丑陋”正是它的目的:它使 intent 明确——正在实施严格的评估。
    【解决方案4】:

    您正在创建带有隐式参数的函数,这不一定是最佳实践。在您的示例中,隐式参数是i。另一种返工方法是:

    library(functional)
    myprint <- function(x) print(x)
    funs <- lapply(1:10, function(i) Curry(myprint, i))
    funs[[1]]()
    # [1] 1
    funs[[2]]()
    # [1] 2
    

    在这里,我们使用Curry 显式指定函数的参数。请注意,我们本可以直接将 print 柯里化,但此处并非用于说明目的。

    Curry 使用预先指定的参数创建新版本的函数。这使得参数规范明确并避免了您遇到的潜在问题,因为Curry 强制评估(有一个版本不这样做,但在这里没有帮助)。

    另一种选择是捕获父函数的整个环境,复制它,并使其成为新函数的父环境:

    funs2 <- lapply(
      1:10, function(i) {
        fun.res <- function() print(i)
        environment(fun.res) <- list2env(as.list(environment()))  # force parent env copy
        fun.res
      }
    )
    funs2[[1]]()
    # [1] 1
    funs2[[2]]()
    # [1] 2
    

    但我不建议这样做,因为您可能会复制一大堆您甚至可能不需要的变量。更糟糕的是,如果你有嵌套的函数层来创建函数,这会变得更加复杂。这种方法的唯一好处是你可以继续你的隐式参数规范,但同样,这对我来说似乎是不好的做法。

    【讨论】:

    • 谢谢,我认为这只是我需要的提示(关于隐式参数依赖)!这本质上不是归结为使用闭包还是在另一个函数中提取所有内容?我想,我最初开始这种风格是因为 Hadley 提倡闭包 (adv-r.had.co.nz/Functional-programming.html)。
    • 关于具体实现,返回Curry(myprint, i)是否等同于force(i); function() myprint(i)?我有点犹豫是否要让我的编程风格中如此不可或缺的一部分依赖于非标准包。考虑例如不了解函数式编程的同事的可读性。
    • 闭包最常见的用途是创建具有持久但可修改状态的函数。您可以使用它们为函数指定隐式参数,但同样,这似乎不是一个好习惯。
    • @jhin,重新等价,是的,在某种程度上 1 + 1 和 3 - 1 是等价的。我仍然更喜欢更明确的方法。请注意,尽管 R 中的函数调用很昂贵,因此这种类型的模式并不常见。
    猜你喜欢
    • 2015-09-06
    • 2019-01-07
    • 2016-12-14
    • 1970-01-01
    • 2021-12-25
    • 2018-04-01
    • 2018-03-31
    • 1970-01-01
    • 2021-08-12
    相关资源
    最近更新 更多