下面是我的代码和结果。第一次尝试会引发错误,第二次尝试会返回结果而不引发错误。我对这个过程很好奇。
- 定义函数
tmp_func <- function(x = 1)
{
x <- x+1
if(!is.null(y=NULL))
{
}
return(x)
}
- 第一次尝试
c <- tmp_func()
# Error in is.null(y = NULL) :
# supplied argument name 'y' doe not match 'x'
- 第二次尝试
c <- tmp_func()
c
#[1] 2
6
1 个回答
1
第二次运行此函数时,它会被 R 的 JIT 编译器编译为字节代码。您看到的效果是编译器处理原始函数的方式的一个怪癖,例如is.null()
带有特殊值的常量参数,例如NULL
,TRUE
或FALSE
。
is.null()
不接受y
争论
首先要理解的是,x
错误消息中引用的不是x
函数中的 ,而是x
期望is.null()
作为参数的 。函数签名是:
is.null
# function (x) .Primitive("is.null")
每当我们向原始函数提供一个它不期望的命名参数时,我们就可以使这个特定的错误出现:
is.null(y = NULL)
# Error in is.null(y = NULL) :
# supplied argument name 'y' does not match 'x'
abs(y = 1)
# Error in abs(y = 1) : supplied argument name 'y' does not match 'x'
所以is.null(y=NULL)
应该引发错误。要分配NULL
并y
同时评估是否y
是NULL
,您需要is.null(y <- NULL)
.
让我们将函数的参数重命名为a
以减少 周围的任何歧义x
,并删除不必要的加法操作:
tmp_func <- function(a = 1) {
if (!is.null(y = NULL)) {}
return(a)
}
tmp_func()
# Error in is.null(y = NULL) :
# supplied argument name 'y' does not match 'x
tmp_func()
# [1] 1
我们可以看到它仍然是抱怨x
,而不是a
。
JIT编译器的作用
问题是为什么我们第二次没有看到这个错误。原因是 R 有一个即时 (JIT),它将常用函数编译为字节代码。您可以通过以下方式检查您的级别设置:
oldJit <- compiler::enableJIT(0)
oldJit
# [1] 3
compiler::enableJIT(oldJit) # revert to previous value
我怀疑至少会是2
:
在第 1 级,大型函数将在首次使用之前进行编译。在第 2 级,小函数也会在第二次使用之前进行编译。 ()
编译函数会改变它处理该表达式的方式
第二次运行函数时,它会被编译。如果我们在第一次和第二次调用后打印函数源代码,我们可以看到这一点:
tmp_func <- function(a = 1) {
if (!is.null(y = NULL)) {}
return(a)
}
tmp_func()
# Error in is.null(y = NULL) :
# supplied argument name 'y' does not match 'x
tmp_func # print source
# function(a = 1) {
# if (!is.null(y = NULL)) {}
# return(a)
# }
tmp_func()
# [1] 1
tmp_func # print source again
# function(a = 1) {
# if (!is.null(y = NULL)) {}
# return(a)
# }
# <bytecode: 0x3f42e28>
请注意,最后一行显示该函数现已编译为字节码。编译器似乎不关心参数名称,因为它跳过 R 函数,并且它与.Primitive("is.null")
C 代码中的接口方式也不关心。我们可以在中看到常量NULL
参数是一种特殊情况(p6):
某些常量值,例如
TRUE
、FALSE
、 和NULL
经常出现在代码中。提供和使用加载这些的特殊说明可能会很有用。
您可以自己测试一下。is.null(y = "a")
例如,如果将其更改为,则编译代码的行为方式与解释代码相同。同样,如果您向非原始函数提供未使用的参数,编译器会引发与解释器相同的错误。
但是,在常量NULL
参数和原始函数的这种特殊情况下,编译器会忽略参数名称。我们可以在生成的指令中看到(注意LDNULL.OP
加载
NULL
常量):
compiler::disassemble(tmp_func)
list(12L, BASEGUARD.OP, 1L, 6L, LDNULL.OP, ISNULL.OP,
NOT.OP, 4L, BRIFNOT.OP, 5L, 14L, LDNULL.OP, GOTO.OP, 15L,
LDNULL.OP, POP.OP, GETVAR.OP, 7L, RETURN.OP)
从技术上讲,这是一个编译器错误
这是奇怪的行为,我认为从技术上讲这是一个编译器错误。我们可以通过这个例子看到:
f <- \(x) is.null(x = NULL) # valid
g <- \(x) is.null(y = NULL) # invalid
compiler::disassemble(f)
# list(12L, BASEGUARD.OP, 0L, 6L, LDNULL.OP, ISNULL.OP, RETURN.OP)
compiler::disassemble(g)
# list(12L, BASEGUARD.OP, 0L, 6L, LDNULL.OP, ISNULL.OP, RETURN.OP)
第二个字节码不应与第一个字节码相同。但这是因为参数的名称被忽略了。
然而,is.null(y = NULL)
这并不是我真正担心的表达方式。此问题似乎仅影响提供了错误命名的常量参数(即TRUE
,FALSE
或 )的原始函数NULL
。因此,就编译器错误而言,在我看来这并不是一个非常重要的错误。无论如何,如果您想确保您的代码永远不会被 JIT 编译,或者确定某些情况是否是由 JIT 编译引起的,您可以更改 JIT:
compiler::enableJIT(0)
4
-
1很好的答案!这正是我一直在寻找的!我问这个问题是因为当我测试我的函数时,我注意到与此类似的参数之一存在拼写错误,但它仍然给出了正确的结果。所以我把它简化成这个小例子。我认为这可能与编译器有关。感谢您清晰的解释!
–
-
1@Palantir 谢谢 – 这是一个有趣的问题。我并不是想说你的最小的、可重现的例子不是一个好例子。我对我的答案进行了一些编辑,以明确我所说的
is.null(y=NULL)
没有意义的地方,我的观点是,在编译器错误领域,这对我来说似乎不太重要。我认为这在技术上是一个错误,并且可以作为 GitHub 问题提出 – 我不熟悉该compiler
包的内部结构,但鉴于它仅适用于三种特殊情况,它可能很容易修复。
– -
1是的,我同意你的看法。我认为这个错误通常很小,因为在大多数情况下代码都被很好地定义和检查。我会尝试在github上提出问题,看看他们是否感兴趣~
– -
@Palantir 出于兴趣,您是否将此报告为错误?我认为这将是该包的 GitHub 问题
compiler
,但实际上它现在是一个核心 R 包,因此大概是此处列出的过程:
–
|
–
–
=
和之间的细微差别有关<-
(将“=”替换为“<-”并且没有错误,但为什么它第二次或更多次不抛出错误是奇怪的)。–
–
Rscript
第一个调用的代码块时总是会出错并且永远不会到达下一个调用。检查在线编译器:。您是否在 Bash、CMD 或 PowerShell 中的 RStudio 或 R.exe 终端会话等 IDE 中运行代码,以保留环境并允许您运行第二次调用?–
|