下面是我的代码和结果。第一次尝试会引发错误,第二次尝试会返回结果而不引发错误。我对这个过程很好奇。

  • 定义函数
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
    第二次尝试是什么意思?你如何运行这段代码?


    – 

  • @Parfait 创建此函数后,第一次尝试是运行 tmp_func() 并引发错误。但是,当我再次运行“tmp_func”时,它会返回该值。完整的过程在描述中进行了描述。你可以尝试一下。谢谢!


    – 


  • 那是……奇怪的。我认为这可能与=之间的细微差别有关<-(将“=”替换为“<-”并且没有错误,但为什么它第二次或更多次不抛出错误是奇怪的)。


    – 


  • @Edward 是的,这真的很奇怪。我尝试了几种不同的方法,没有一个像这个那样有效。非常好奇运行这段代码时R核心如何与内存交互。


    – 

  • 我仍然无法重现,因为每次我运行Rscript第一个调用的代码块时总是会出错并且永远不会到达下一个调用。检查在线编译器:。您是否在 Bash、CMD 或 PowerShell 中的 RStudio 或 R.exe 终端会话等 IDE 中运行代码,以保留环境并允许您运行第二次调用?


    – 



1 个回答
1

第二次运行此函数时,它会被 R 的 JIT 编译器编译为字节代码。您看到的效果是编译器处理原始函数的方式的一个怪癖,例如is.null()带有特殊值的常量参数,例如NULL,TRUEFALSE

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)应该引发错误。要分配NULLy同时评估是否yNULL,您需要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):

某些常量值,例如TRUEFALSE、 和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 包,因此大概是此处列出的过程:


    –