Search code examples
rrlangnse

Unquoting the loop variable in `rlang::expr`


I came across unexpected behavior when using operator !! to iteratively construct expressions that involve a loop variable.

Creating an expression from the loop variable itself, e.g.,

X <- list()
for( i in 1:3 )
  X[[i]] <- rlang::expr( !!i )
str(X)
# List of 3
#  $ : int 1
#  $ : int 2
#  $ : int 3

works as expected. However, attempting to construct any complex expression involving the loop variable, such as

Y <- list()
for( i in 1:3 )
    Y[[i]] <- rlang::expr( 1 + (!!i) )
str(Y)
# List of 3
#  $ : language 1 + 3L
#  $ : language 1 + 3L
#  $ : language 1 + 3L

seems to produce expressions that contain the final value of the loop variable. It's almost as if the expressions get captured as quote( 1 + (!!i) ) during the loop, but the unquoting via !! happens only after the entire loop is executed, as opposed to at every iteration.

Can somebody please shed some light on this behavior? Is this a bug? The intended goal is to capture unevaluated expressions 1+1, 1+2 and 1+3 with a loop. The expected output of Y would therefore be

# List of 3
#  $ : language 1 + 1L
#  $ : language 1 + 2L
#  $ : language 1 + 3L

Side note: the extra parentheses around !!i are there to address the potential issue of operator precedence.

Using rlang 0.2.1.


Solution

  • It seems that for creates some sort of environment / frame where evaluation takes place at the end (for the last i). One possible way to deal with this is to avoid for and use purrr:: or lapply.

    Y <- purrr::map(1:3, ~ rlang::expr( 1 + (!! .) )) 
    str(Y)
    #> List of 3
    #>  $ : language 1 + 1L
    #>  $ : language 1 + 2L
    #>  $ : language 1 + 3L
    
    Y <- lapply(1:3, function(i) rlang::expr( 1 + (!! i) )) 
    str(Y)
    #> List of 3
    #>  $ : language 1 + 1L
    #>  $ : language 1 + 2L
    #>  $ : language 1 + 3L
    

    while works in console but it fails when used with reprex (not shown).

    Y <- list()
    i <- 1L
    while (i <= 3) {
        Y[[i]] <- rlang::expr( 1 + (!!i) )
        i <- i + 1L
    }
    str(Y)
    #> List of 3
    #>  $ : language 1 + 1L
    #>  $ : language 1 + 2L
    #>  $ : language 1 + 3L