16.6 魔法

我们使用文本编辑器所编写的源代码只不过是一堆字符串。当我们运行代码时,R需要解读一下这些字符串再执行相应的操作。这个过程首先从把字符串变成若干的语言变量类型开始。有时我们可能要逆转此过程,即把语言变量转换为字符串。

这两种任务都是比较高级的话题,就像一种黑暗魔法。正如每一部电影中的魔法一样,如果使用前没有理解清楚,你最终会承受糟糕的、意料之外的结果。另一方面,这里有一些秘密武器,使用它们须谨慎且有见地。

16.6.1 将字符串转换成代码

每当你在命令行中输入一行代码时,R会把这个字符串转换成它能理解的东西。以下是一个简单的反正切函数的调用:

  1. atan(c(-Inf, -1, 0, 1, Inf))
  2. ## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708

可以通过使用quote函数来慢镜头细看这行代码到底发生了什么。quote接受一个像上面一行的函数作为输入参数,并将返回一个call类的对象,它代表一个尚未进行计算(unevaluated)的函数调用的对象:

  1. (quoted_r_code <- quote(atan(c(-Inf, -1, 0, 1, Inf))))
  2. ## atan(c(-Inf, -1, 0, 1, Inf))
  3. class(quoted_r_code)
  4. ## [1] "call"

接下来,R将对此调用进行计算。可使用eval函数来模仿此步骤:

  1. eval(quoted_r_code)
  2. ## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708

一般情况下,为执行你键入的代码,R会运行类似eval(quote(the stuff you typed at the command line))的函数。

为了更好地了解call类型,把它转换成一个列表:

  1. as.list(quoted_r_code)
  2. ## [[1]]
  3. ## atan
  4. ##
  5. ## [[2]]
  6. ## c(-Inf, -1, 0, 1, Inf)

第一个元素是被调用的函数,其他元素都是我们要传递给它的参数。

记住重要的是:在R中几乎一切都是函数。这有点夸张,但像+的运算符、switchiffor的语言结构以及分配和索引都是函数:

  1. vapply(
  2. list(`+`, `if`, `for`, `<-`, `[`, `[[`),
  3. is.function,
  4. logical(1) )
  5. ## [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参数:

  1. parsed_r_code <- parse(text = "atan(c(-Inf, -1, 0, 1, Inf))")
  2. class(parsed_r_code)
  3. ## [1] "expression"

使用eval来计算引用的R代码:

  1. eval(parsed_r_code)
  2. ## [1] -1.5708 -0.7854 0.0000 0.7854 1.5708

警告

这种与字符串混合计算的小技巧比较好用,但由此产生的代码通常是脆弱和难于调试的,使你的代码难以维护。这就是我上面提到“僵尸覆灭”的含义。

16.6.2 把代码转换成字符串

有时,我们要解决的问题正好相反,即把代码转换成为字符串。最常见的场景是为了把变量的名称传递给函数。base包中的直方图绘图函数hist包含一个默认的标题,它能告诉你数据变量的名称:

  1. random_numbers <- rt(1000, 2)
  2. hist(random_numbers)

我们也可以自己重新实现这个功能,只需使用两个函数:substitutedeparsesubstitue接受一些代码并返回一个语言对象。这通常会是一个call对象,正如我们使用quote所创建的一样,但偶尔也会是一个name对象,它是一种能保存变量名的特殊类型。(不用担心其中的具体细节,本节被称为“魔法”并非偶然。)

下一步就是把这个语言对象转换为字符串,即所谓的deparsing。当你检查函数的用户输入时,此技术能提供有用的错误信息。让我们来看看deparse-substitue组合在实际中的效果:

  1. divider <- function(numerator, denominator)
  2. {
  3. if(denominator == 0)
  4. {
  5. denominator_name <- deparse(substitute(denominator))
  6. warning("The denominator, ", sQuote(denominator_name), ", is zero.")
  7. }
  8. numerator / denominator
  9. }
  10. top <- 3
  11. bottom <- 0
  12. divider(top, bottom)
  13. ## Warning: The denominator, 'bottom', is zero.
  14. ## [1] Inf

substitute在与eval一起使用时还有另一项技巧。给eval传递一个环境变量或数据框,你就可告诉R到哪里去查找要计算的表达式了。

举个简单的例子,我们可以用这一招来取得hafu数据集中Gender栏的水平值:

  1. eval(substitute(levels(Gender)), hafu)
  2. ## [1] "F" "M"

这也恰恰是with函数的工作原理:

  1. with(hafu, levels(Gender))
  2. ## [1] "F" "M"

事实上,有很多函数都使用了此技巧:subset在好几个地方都用到它,lattice图形系统以此技巧来解析公式。此技巧还有一些其他的变化,可参考Thomas Lumley的“Standard nonstandard evaluation rules”。