Non standard evaluation from another function in R

后端 未结 3 1149
梦毁少年i
梦毁少年i 2021-02-02 12:38

Here is an example from Hadley\'s advanced R book:

sample_df <- data.frame(a = 1:5, b = 5:1, c = c(5, 3, 1, 4, 1))

subset2 <- function(x, condition) {
  c         


        
3条回答
  •  臣服心动
    2021-02-02 12:56

    tl;dr

    When subset2() is called from within subscramble(), condition_call's value is the symbol condition (rather than the call a >= 4 that results when it is called directly). subset()'s call to eval() searches for condition first in envir=x (the data.frame sample_df). Not finding it there, it next searches in enclos=parent.frame() where it does find an object named condition.

    That object is a promise object, whose expression slot is a >= 4 and whose evaluation environment is .GlobalEnv. Unless an object named a is found in .GlobalEnv or further up the search path, evaluation of the promise then fails with the observed message that: Error in eval(expr, envir, enclos) : object 'a' not found.


    Detailed explanation

    A nice way to discover what's going wrong here is to insert a browser() call right before the line at which subset2() fails. That way, we can call it both directly and indirectly (from within another function), and examine why it succeeds in the first case and fails in the second.

    subset2 <- function(x, condition) {
      condition_call <- substitute(condition)
      browser()
      r <- eval(condition_call, x, parent.frame())  ## <- Point of failure
      x[r, ]
    }
    

    Calling subset2() directly

    When a user calls subset2() directly, condition_call <- substitute(condition) assigns to condition_call a "call" object containing the unevaluated call a >= 4. This call is passed in to eval(expr, envir, enclos), which needs as its first argument a symbol that evaluates to an object of class call, name, or expression. So far so good.

    subset2(sample_df, a >= 4)
    ## Called from: subset2(sample_df, a >= 4)
    Browse[1]> is(condition_call)
    ## [1] "call"     "language"
    Browse[1]> condition_call
    ## a >= 4
    

    eval() now sets to work, searching for the values of any symbols contained in expr=condition_call first in envir=x and then (if needed) in enclos=parent.frame() and its enclosing environments. In this case, it finds the symbol a in envir=x (and the symbol >= in package:base) and successfully completes the evaluation.

    Browse[1]> ls(x)
    ## [1] "a" "b" "c"
    Browse[1]> get("a", x)
    ## [1] 1 2 3 4 5
    Browse[1]> eval(condition_call, x, parent.frame())
    ## [1] FALSE FALSE FALSE  TRUE  TRUE
    

    Calling subset2() from within subscramble()

    Within the body of subscramble(), subset2() is called like this: subset2(x, condition). Fleshed out, that call is really equivalent to subset2(x=x, condition=condition). Because its supplied argument (i.e. the value passed to the formal argument named condition) is the expression condition, condition_call <- substitute(condition) assigns to condition_call the symbol object condition. (Understanding that point is pretty key to understanding exactly how the nested call fails.)

    Since eval() is happy to have a symbol (aka "name") as its first argument, once again so far so good.

    subscramble(sample_df, a >= 4)
    ## Called from: subset2(x, condition)
    Browse[1]> is(condition_call)
    ## [1] "name"      "language"  "refObject"
    Browse[1]> condition_call
    ## condition
    

    Now eval() goes to work searching for the unresolved symbol condition. No column in envir=x (the data.frame sample_df) matches, so it moves on to enclos=parent.frame() For fairly complicated reasons, that environment turns out to be the evaluation frame of the call to subscramble(). There, it does find an object named condition.

    Browse[1]> ls(x)
    ## [1] "a" "b" "c"
    Browse[1]> ls(parent.frame()) ## Aha! Here's an object named "condition"
    ## [1] "condition" "x"
    

    As an important aside, it turns out there are several objects named condition on the call stack above the environment from which browser() was called.

    Browse[1]> sys.calls()
    # [[1]]
    # subscramble(sample_df, a >= 4)
    # 
    # [[2]]
    # scramble(subset2(x, condition))
    # 
    # [[3]]
    # subset2(x, condition)               
    # 
    Browse[1]> sys.frames()
    # [[1]]
    #    ## <- Envt in which `condition` is evaluated
    # 
    # [[2]]
    # 
    # 
    # [[3]]
    #    ## <- Current environment
    
    
    ## Orient ourselves a bit more
    Browse[1]> environment()            
    # 
    Browse[1]> parent.frame()           
    # 
    
    ## Both environments contain objects named 'condition'
    Browse[1]> ls(environment())
    # [1] "condition"      "condition_call" "x"             
    Browse[1]> ls(parent.frame())
    # [1] "condition" "x"  
    

    To inspect the condition object found by eval() (the one in parent.frame(), which turns out to be the evaluation frame of subscramble()) takes some special care. I used recover() and pryr::promise_info() as shown below.

    That inspection reveals that condition is a promise whose expression slot is a >= 4 and whose environment is .GlobalEnv. Our search for a has by this point moved well past sample_df (where a value of a was to be found), so evaluation of the expression slot fails (unless an object named a is found in .GlobalEnv or somewhere else farther up the search path).

    Browse[1]> library(pryr) ## For is_promise() and promise_info()  
    Browse[1]> recover()
    # 
    # Enter a frame number, or 0 to exit   
    # 
    # 1: subscramble(sample_df, a >= 4)
    # 2: #2: scramble(subset2(x, condition))
    # 3: #1: subset2(x, condition)
    # 
    Selection: 1
    # Called from: top level 
    Browse[3]> is_promise(condition)
    # [1] TRUE
    Browse[3]> promise_info(condition)
    # $code
    # a >= 4
    # 
    # $env
    # 
    # 
    # $evaled
    # [1] FALSE
    # 
    # $value
    # NULL
    # 
    Browse[3]> get("a", .GlobalEnv)
    # Error in get("a", .GlobalEnv) : object 'a' not found
    

    For one more piece of evidence that the promise object condition is being found in enclos=parent.frame(), one can point enclos somewhere else farther up the search path, so that parent.frame() is skipped during condition_call's evaluation. When one does that, subscramble() again fails, but this time with a message that condition itself was not found.

    ## Compare
    Browse[1]> eval(condition_call, x, parent.frame())
    # Error in eval(expr, envir, enclos) (from #4) : object 'a' not found
    
    Browse[1]> eval(condition_call, x, .GlobalEnv)
    # Error in eval(expr, envir, enclos) (from #4) : object 'condition' not found
    

提交回复
热议问题