惰性求值

如果能够理解 R 函数的运行机制,那么对 R 的运行机制就能够掌握十之八九。通过前
几章的学习,我们掌握了一些最常用的基本函数的用法,但是可能对其内部机制仍有疑惑。
比如创建如下的函数:
test0 <- function(x, y) {
if (x > 0) x else y
}
这个函数有点特殊,看似只有当 x 不大于 0 时才需要 y。如果只把一个正数赋值给 x 而忽
略 y,这个函数会因为没有为所有定义的参数赋值而报错么?我们调用这个函数运行一下看看:
test0(1)
## [1] 1
即使没有对 y 赋值,函数仍能够运行。看来在调用函数时不必对所有参数进行赋值,
只对必需的参数赋值即可。如果给 x 赋一个负数,函数便需要参数 y 才能运行:
test0(-1)
## Error in test0(-1): 缺少参数"y",也没有缺省值
由于没有指定 y 的值,函数便终止运行,并且返回报错信息:参数 y 缺失。
通过前面的示例,我们知道了函数不需要指定所有参数,因为有些参数不需要返回值。
如果非要指定在函数中没有用到的参数,那么,程序是会在调用函数之前计算它们,还是
根本不会计算它们呢?接下来,我们利用 stop( ) 函数来一探究竟。将 stop( ) 函数放
在参数 y 的位置,如果在任何地方以任何方式执行了这个表达式,函数都会在返回 x 的值
之前终止运行:
test0(1, stop("Stop now"))
## [1] 1
结果显示 stop( ) 函数没有起作用,说明此函数根本不会被执行。如果我们给 x 赋
一个负数,函数便会终止运行:
test0(-1, stop("Stop now"))
## Error in test0(-1, stop("Stop now")): Stop now
很明显,这个语句中的 stop( ) 被执行了。至此,这个机制也就非常清晰了。在函
数调用时,参数的值只有被实际用到时才会被执行。这个机制被称作惰性求值,也就是在
函数调用中,参数被“懒惰地计算”,即按需计算。
如果你对惰性求值还不是很清楚,可能会认为下面这个函数的调用非常耗时,并且可能占
用计算机全部内存。惰性求值则避免了上述情况的发生,这是因为在计算表达式if (x > 0) x
else y 时,R 并不会计算 rnorm(10000000),可以通过 system.time( )函数查看运算
时间进行证实:
system.time(rnorm(10000000))
## user system elapsed
## 0.91 0.01 0.92
生成 1000 万个随机数并不是一项容易的工作,需要花费超过 1 秒的时间。相反,计算
一个数应该是 R 能做的最简单的事,耗时小到连计时函数都无法得出精确结果:
system.time(1)
## user system elapsed
## 0 0 0
如果我们为如下表达式计时,在了解 test0( ) 函数的运算逻辑和惰性求值机制的情
况下,应该很容易猜到结果为 0:
system.time(test0(1, rnorm(10000000)))
## user system elapsed
## 0 0 0
另一个有可能触发惰性求值机制的情况是参数默认值。具体来说,函数参数的默认值
是其默认表达式,在表达式被计算之前,其值是不可用的。请看如下函数:
test1 <- function(x, y = stop("Stop now")) {
if (x > 0) x else y
}
我们将函数 stop( ) 赋予 y 作为默认值。如果此处惰性求值不适用,即无论是否被
用到,系统都会计算 y,只要调用 test1( ) 时没有另外指定 y 的值,都会返回错误信息。
然而,若此处惰性求值适用,那么,调用 test1( ) 时给 x 赋予一个正数,函数便不会报
错,因为 y 的表达式 stop( )不会被计算。
我们来做个试验,看看哪种情况会发生。赋给 x 一个正数,并调用 test1( ) 函数:
test1(1)
## [1] 1
输出结果说明此处惰性求值是适用的。函数只用到了参数 x ,而没有执行 y 的默认表
达式。如果在赋给 x 一个负数的情况下调用 test1( ) 函数,函数应该会终止运行:
test1(-1)
## Error in test1(-1): Stop now
前面的示例演示了惰性求值的优点:节省时间并且避免了不必要的计算。另外,它还
允许对函数参数默认值进行更灵活的说明。例如你可以在一个参数的表达式中使用另一个
参数:
test2 <- function(x, n = floor(length(x) / 2)) {
x[1:n]
}
这让你能够以更合理或更可取的方式设置函数的默认行为,当参数没有默认值时,仍
然可以自定义。
如果我们在不指定 n 的情况下调用 test2,函数会默认提取 x 的前一半部分元素:
test2(1:10)
## [1] 1 2 3 4 5
这个函数还是灵活的,你可以无视 n 的默认表达式,另外指定 n 的值:
test2(1:10, 3)
## [1] 1 2 3
惰性求值是一把双刃剑,有利同样也有弊。在调用函数时,其参数只被解析不被计算,
所以我们只能确定参数表达式在语法上是正确的,但很难确定它的有效性。
例如一个未定义的变量出现在参数的默认值中,创建函数的时候不会出现警告或报错。
下面我们创建一个与 test2( ) 相同的函数 test3( ),但是把 n 的表达式中的 x 误写为
未定义的变量 m:
test3 <- function(x, n = floor(length(m) / 2)) {
x[1:n]
}
在创建 test3 函数时,不会出现警告或报错,因为在调用 test3( ) 之前,不会计
算 floor(length(m)/2),并且 n 的值由 1:n 确定。只有当实际调用时,函数才会终止运行:
test3(1:10)
## Error in test3(1:10): 找不到对象'm'
如果我们在调用 test3( ) 之前定义了 m ,函数便有效,但是以一个意外的方式:
m <- c(1, 2, 3)
test3(1:10)
## [1] 1
这个例子更清晰地展示了惰性求值:
test4 <- function(x, y = p) {
p <- x + 1
c(x, y)
}
注意 y 的默认值为 p ,正如之前的示例一样,p 在函数被调用前是未定义的。这两个
示例的显著区别在于第 2 个参数的默认值被定义的位置。在前例中,p 是在调用函数之前
定义的,而在本例中,p 是在函数内部使用 y 之前定义的。
我们来看看调用这个函数会发生什么:
test4(1)
## [1] 1 2
结果显示,函数是可行的,没有报错,更没有终止。整理一下 test4(1) 运行的详细
过程,就很容易理解其中的逻辑了。
1.找到名为 test4 的函数。
2.匹配给定参数,但是没有计算 x 和 y 。
3.p<-x+1 表达式计算 x+1 并且将值赋给新的变量 p。
4.c(x,y) 表达式计算 x 和 y,其中 x 为 1,y 为 p,即 x+1 的计算结果 2。
5.函数返回数值向量 c(1, 2)。
因此,在 test4(1) 的整个计算过程中没有出现警告或报错,因为并没有违反任何规
则。其中最重要的技巧就是在用到 y 之前定义了 p。
这个示例有助于解释惰性计算的工作方式,但却不是一个好方法。并不推荐用这种方
式编写函数,因为这种技巧使函数的运行方式变得不那么直观。一个好的做法是简化参数,
避免使用函数外未定义的变量。否则由于依赖于外部环境,很难对函数的运行进行预测及
调试。
尽管如此,惰性求值也还是有一些巧妙的用法。例如,stop( ) 可以与 switch( ) 一
起用在函数的最后一个参数中,以便在没有匹配时终止函数的运行。下面的函
数 check_input( ) 用 switch( ) 来控制输入参数 x ,使其只接受 y 或 n,并且在输
入其他字符串时终止运行:
check_input <- function(x) {
switch(x,
y = message("yes"),
n = message("no"),
stop("Invalid input"))
}
当 x 取值为 y 时,返回值为 yes:
check_ _input("y")
## yes
当 x 取值为 n 时,返回值为 no:
check_ _input("n")
## no
否则,函数终止运行:
check_ _input("what")
## Error in check_input("what"): Invalid input
本例中函数可以运行,因为 stop( ) 作为 switch( ) 的参数被“懒惰地计算”。
作为对这个例子的总结,这里需要提醒的是不能过度依赖解析器检查代码。它只能检
查代码的语法而不能判断代码是否写得好。为了避免惰性求值隐含的潜在缺陷,有必要对
函数进行仔细检查,以确保输入值能够被正确处理。

posted @ 2019-02-11 10:07  NAVYSUMMER  阅读(538)  评论(0编辑  收藏  举报
交流群 编程书籍