- 参考:
- R的内部机制 - 王诗翔 (shixiangwang.github.io)[1]
- 19 函数进阶 | R语言教程 (pku.edu.cn)[2]
前言
其实之前读了李东风老师的内容,感觉收获颇丰;但因为自己的业务逻辑过于简单,渐渐又荒废掉了。
最近碰巧看到王诗翔的这篇文章,再次学习,顺便将笔记从yuque 发至公众号。共勉。
因为内容有些多,拆成两个部分介绍。
第一部分:
- 惰性求值 (Lazy evaluation)
- 词法作用域 (Lexical scoping)
惰性求值
这里引用李东风老师的原话:
★R函数在调用执行时,
除非用到某个形式变量的值才求出其对应实参的值
。这一点在实参是常数时无所谓, 但是如果实参是表达式就不一样了。形参缺省值也是只有在函数运行时用到该形参的值时才求值。 ”
这个特点可以用下面的例子理解,比如行参的默认值不是常量而是一个表达式:
代码语言:javascript复制f <- function(x, y=ifelse(x>0, TRUE, FALSE)){
x <- -111
if(y) x*2 else x*10
}
f(5)
这时候的输出结果并不是,10,而是-1100。
这是因为形参y 并没按x=5 被赋值为TRUE, 而是到函数体中第二个语句才被求值, 这时x 的值已经变成了-111, 故y的值是FALSE。
同样利用这个特性,如果形参在函数主体中并没有被使用,则程序也不会报错:
代码语言:javascript复制test0 <- function(x, y) {
if (x > 0) x else y
}
test0(1)
#> [1] 1
我们可以简单的验证一下:
代码语言:javascript复制test0(1, stop("Stop Now!"))
#> [1] 1
test0(-1, stop("Stop Now!"))
#> Error in test0(-1, stop("Stop Now!")): Stop Now!
程序只在第二个语句中起作用了,也就是满足y 在函数中被调用的条件,x 不大于0 的情况。
为了记录形参是否在主体中被使用,在函数内部, 用missing(x)
对形参x判断用户是否没有提供对应的实参, 对位置形参和有缺省值的形参都适用。
> a <- function(x=3,y) missing(x)
> a(1,3)
[1] FALSE
> a(x = 1,3)
[1] FALSE
> a(3)
[1] FALSE
> a(y=3)
[1] TRUE
其判断用户是否没有提供对应的实参,如果提供了则为F,没有则是T。
懒惰求值的好处是,实现了我们程序的按需分配。
比如说有人上来就要十万块钱的烤肉饭,他连钱都没掏出来,我凭什么给他做呢?
代码语言:javascript复制> test0 <- function(x, y) {
if (x > 0) x else y
}
> system.time(test0(1, rnorm(10000000)))
用户 系统 流逝
0 0 0
> system.time(rnorm(10000000))
用户 系统 流逝
0.695 0.033 0.740
因为只有在函数运行时用到该形参的值时才求值,所以如果参数表达式语法上没有问题,而实际值存在问题,也只有在调用该函数时才会发生报错:
代码语言:javascript复制test3 <- function(x, n=floor(length(m) / 2)){
x[1:n]
}
test3(1:10)
#> Error in test3(1:10): object 'm' not found
不过这种tradeoff,个人觉得牺牲并不算太大,还是蛮赚的。
词法作用域
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
这里举一个例子:
代码语言:javascript复制x <- -1
f0 <- function(x){
f1 <- function(){
x 100
}
f1()
}
R语言允许在函数体内定义函数,其中内嵌的函数f1() 称为一个closure(闭包)。
内嵌的函数体内在读取某个变量值时, 如果此变量在函数体内还没有被赋值,它就不是局部的,会向定义的外面一层查找;外层一层找不到,就继续向外查找,直到找到为止,如果在global 环境中还没有该变量,则会抛出异常。
上面例子f1()定义中的变量x不是局部变量, 就向外一层查找, 找到的会是f0的自变量x,而不是全局空间中x。
代码语言:javascript复制f0(1)
## [1] 101
这样的变量查找规则叫做动态查找。即函数运行中需要使用某个变量时, 从其定义时的环境向外层逐层查找, 而不仅仅只是在调用时的环境中查找。
再看看下面的例子:
代码语言:javascript复制f0 <- function(){
f1 <- function(){
x <- -1
f2 <- function(){
x 100
}
f2()
}
x <- 1000
f1()
}
f0()
请思考0.00001ms,答案是多少?
其中f2()
运行时, 用到的x
是f1()
函数体内的局部变量x=-1
, 而不是被调用时f0()
函数体内的局部变量x=1000
, 所以结果是-1 100 = 99
。
f0 <- function(){
f1 <- function(){
x <- -1
f2 <- function(){
x 100
}
x <- 99
f2()
}
x <- 1000
f1()
}
f0()
此时的运行结果就是99
,这是因为,在调用f2
之前,其只会访问变量x 的当前值99
,而不是历史值-1
。
“句法作用域”指的是函数调用变量时,查找其定义时的变量对应的存储空间, 而不是定义时变量所取的历史值。
函数运行时在找到某个变量对应的存储空间后, 会使用该变量的当前值,而不是函数定义的时候该变量的历史值。
简单来说,就是越靠近函数的变量,才是函数使用的那个变量。
有时我们还会讨论到函数作用域,也即在函数的内部,我们能够使用外部变量和函数,但外部不能使用内部变量和函数(除非使用<<-
创建全局变量)。可以参考:[[122-R编程19-赋值运算符]] 此外,函数每一次运行都会刷新其内部的子环境。
参考资料
[1]
R的内部机制 - 王诗翔 (shixiangwang.github.io): https://shixiangwang.github.io/home/cn/post/2019-11-20-r-mechanism/
[2]
19 函数进阶 | R语言教程 (pku.edu.cn): https://www.math.pku.edu.cn/teachers/lidf/docs/Rbook/html/_Rbook/p-advfunc.html#p-advfunc-lazy