16.6 魔法
我们使用文本编辑器所编写的源代码只不过是一堆字符串。当我们运行代码时,R需要解读一下这些字符串再执行相应的操作。这个过程首先从把字符串变成若干的语言变量类型开始。有时我们可能要逆转此过程,即把语言变量转换为字符串。
这两种任务都是比较高级的话题,就像一种黑暗魔法。正如每一部电影中的魔法一样,如果使用前没有理解清楚,你最终会承受糟糕的、意料之外的结果。另一方面,这里有一些秘密武器,使用它们须谨慎且有见地。
16.6.1 将字符串转换成代码
每当你在命令行中输入一行代码时,R会把这个字符串转换成它能理解的东西。以下是一个简单的反正切函数的调用:
atan(c(-Inf, -1, 0, 1, Inf))
## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708
可以通过使用quote
函数来慢镜头细看这行代码到底发生了什么。quote
接受一个像上面一行的函数作为输入参数,并将返回一个call
类的对象,它代表一个尚未进行计算(unevaluated)的函数调用的对象:
(quoted_r_code <- quote(atan(c(-Inf, -1, 0, 1, Inf))))
## atan(c(-Inf, -1, 0, 1, Inf))
class(quoted_r_code)
## [1] "call"
接下来,R将对此调用进行计算。可使用eval
函数来模仿此步骤:
eval(quoted_r_code)
## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708
一般情况下,为执行你键入的代码,R会运行类似eval(quote(the stuff you typed at the command line))
的函数。
为了更好地了解call
类型,把它转换成一个列表:
as.list(quoted_r_code)
## [[1]]
## atan
##
## [[2]]
## c(-Inf, -1, 0, 1, Inf)
第一个元素是被调用的函数,其他元素都是我们要传递给它的参数。
记住重要的是:在R中几乎一切都是函数。这有点夸张,但像+
的运算符、switch
、if
、for
的语言结构以及分配和索引都是函数:
vapply(
list(`+`, `if`, `for`, `<-`, `[`, `[[`),
is.function,
logical(1) )
## [1] TRUE TRUE TRUE TRUE TRUE TRUE
这样做的结果是,你在命令行键入的任何东西真的是函数调用,这是为什么此输入会变成call
对象的原因。
我们兜了一个大圈说明:有时我们需接受R代码文本,然后让R来执行它。事实上,我们已经看到两个函数对于这种特殊情况完全就是这样做的:assign
接受一个字符串,并使用该名称将值指派给一个变量;与之相反,get
从一个字符串输入中获取一个变量。
除了分配和获取变量之外,我们偶尔可能还需要接受任意R的代码文本并从R中执行它。你可能已经注意到,当使用quote
函数时,我们只是直接把R代码敲进去而没有把它括在括号内。如果输入的是一个字符串(例如一个长度为一的字符向量),问题会稍微不同:我们必须“解析”字符串。当然,这要通过parse
函数来完成。
parse
返回一个expression
对象而不是一个调用。不用紧张,其实expression
基本上就是一个调用列表。
提示
调用和表达式的本质是很深奥的,就像黑暗魔法一样。当你用R大玩“起死回生”之术时,我可不为接下来的“僵尸覆灭”负责。如果你对这些奥秘有兴趣,请阅读随R附带的R Language Definition手册中的第6章。
当以这种方式调用parse
时,必须明确声名text
参数:
parsed_r_code <- parse(text = "atan(c(-Inf, -1, 0, 1, Inf))")
class(parsed_r_code)
## [1] "expression"
使用eval
来计算引用的R代码:
eval(parsed_r_code)
## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708
警告
这种与字符串混合计算的小技巧比较好用,但由此产生的代码通常是脆弱和难于调试的,使你的代码难以维护。这就是我上面提到“僵尸覆灭”的含义。
16.6.2 把代码转换成字符串
有时,我们要解决的问题正好相反,即把代码转换成为字符串。最常见的场景是为了把变量的名称传递给函数。base
包中的直方图绘图函数hist
包含一个默认的标题,它能告诉你数据变量的名称:
random_numbers <- rt(1000, 2)
hist(random_numbers)
我们也可以自己重新实现这个功能,只需使用两个函数:substitute
和deparse
。substitue
接受一些代码并返回一个语言对象。这通常会是一个call
对象,正如我们使用quote
所创建的一样,但偶尔也会是一个name
对象,它是一种能保存变量名的特殊类型。(不用担心其中的具体细节,本节被称为“魔法”并非偶然。)
下一步就是把这个语言对象转换为字符串,即所谓的deparsing
。当你检查函数的用户输入时,此技术能提供有用的错误信息。让我们来看看deparse-substitue
组合在实际中的效果:
divider <- function(numerator, denominator)
{
if(denominator == 0)
{
denominator_name <- deparse(substitute(denominator))
warning("The denominator, ", sQuote(denominator_name), ", is zero.")
}
numerator / denominator
}
top <- 3
bottom <- 0
divider(top, bottom)
## Warning: The denominator, 'bottom', is zero.
## [1] Inf
substitute
在与eval
一起使用时还有另一项技巧。给eval
传递一个环境变量或数据框,你就可告诉R到哪里去查找要计算的表达式了。
举个简单的例子,我们可以用这一招来取得hafu
数据集中Gender
栏的水平值:
eval(substitute(levels(Gender)), hafu)
## [1] "F" "M"
这也恰恰是with
函数的工作原理:
with(hafu, levels(Gender))
## [1] "F" "M"
事实上,有很多函数都使用了此技巧:subset
在好几个地方都用到它,lattice
图形系统以此技巧来解析公式。此技巧还有一些其他的变化,可参考Thomas Lumley的“Standard nonstandard evaluation rules”。