Funções de substituição, como names<-
, parecem não usar avaliação preguiçosa quando chamadas como names(x) <- c("a", "b")
.
Para demonstrar, vamos definir uma função para obter a parte fracionária de um número e uma função de substituição correspondente - mas dentro da função de substituição, inclua uma linha para imprimir o value
argumento neutralizado.
fractional <- function(x) {
x %% 1
}
`fractional<-` <- function(x, value) {
print(rlang::enexpr(value))
invisible(x %/% 1 + value)
}
Agora, se chamarmos fractional<-
diretamente, ele imprime a expressão que demos para value
:
x <- 10.1
`fractional<-`(x, 0.2 + 0.2)
#> 0.2 + 0.2
Mas se o chamarmos no formulário de atribuição, ele imprime o resultado da avaliação da expressão:
x <- 10.1
fractional(x) <- 0.2 + 0.2
#> [1] 0.4
A definição da linguagem explica funções de substituição como:
names(x) <- c("a","b")
é equivalente a
`*tmp*` <- x x <- "names<-"(`*tmp*`, value=c("a","b")) rm(`*tmp*`)
Mas isso não reproduz esse comportamento:
x <- 10.1
`*tmp*` <- x
x <- "fractional<-"(`*tmp*`, value=0.2 + 0.2)
rm(`*tmp*`)
#> 0.2 + 0.2
O que está acontecendo internamente <-
que faz com que value
seja passado para fractional<-
depois de ser avaliado, e há alguma maneira de contornar esse comportamento?
Edição: @SamR destacou que usar substitute
captura a expressão da promessa:
x <- 10.1
`fractional<-` <- function(x, value) {
print(substitute(value))
invisible(x %/% 1 + value)
}
fractional(x) <- 0.2 + 0.2
#> 0.2 + 0.2
Então, claramente eu estava enganado ao assumir que value
estava sendo avaliado antes de ser passado para fractional<-
. No entanto, eu ainda gostaria muito de saber por que base::substitute
funciona como esperado aqui enquanto rlang::enexpr
e amigos não . Afinal, enexpr
usa substitute
internamente:
enexpr <- function(arg) {
.Call(ffi_enexpr, substitute(arg), parent.frame())
}
A depuração no R Studio mostra que, tanto quando chamado na forma de atribuição como fractional(x) <- 0.2 + 0.2
quanto na forma de prefixo "fractional<-"(x, 0.2 + 0.2)
, fractional<-
é passada uma promessa não avaliada para value
:
Isso permanece não avaliado quando chamado na forma de prefixo:
Mas é avaliado após a chamada para enexpr
quando chamado no formulário de atribuição:
Estou pensando se isso tem a ver com o fato de que no formulário de atribuição, a função é chamada por uma função primitiva, <-
? Mas não está claro por que isso faria diferença.
Em R, a forma
é conhecido como atribuição complexa , um termo que também se aplica às várias atribuições de subconjuntos, como:
O interpretador R lida com atribuições complexas no código C subjacente por meio da função applydefine , no arquivo
src/main/eval.c
.O código é difícil de seguir sem saber alguns detalhes de como R é implementado em C, mas essencialmente, quando o analisador encontra
f(x) <- y
, ele reorganiza a expressão antes de avaliá-la. Primeiro, ele acrescenta a<-
no final do nome da funçãof
e cria uma chamada para a função`f<-`
. No entanto, ele não reorganiza simplesmente os símbolos e chama`f<-`(x, y)
. Em vez disso, ele chama`f<-`
com uma variável temporária no lugar dex
e um objeto de promessa no lugar dey
. Há boas razões para isso que veremos a seguir.Antes de entrarmos nisso, vamos confirmar as diferenças entre chamar a função diretamente e por meio de atribuição complexa. Podemos escrever uma função que simplesmente imprime os argumentos com os quais é chamada e deixa
x
como está:Chamando isso diretamente, não temos surpresas:
Mas veja o que acontece quando usamos a sintaxe de atribuição complexa:
Podemos ver que
x
foi substituído por uma variável chamada*tmp*
e que"foo"
foi substituído por um objeto de promessa.A
*tmp*
variável é referida no código C comoR_TmpvalSymbol
, e é escrita na chamada como uma forma de manter cálculos intermediários em caso de atribuições complexas aninhadas. Isso é descrito nos comentários do código:Quanto à promessa ser usada no lugar da expressão do lado direito, os comentários do código explicam que
No entanto, R gosta de usar avaliação preguiçosa (não avaliar código até que seja necessário) e, em vez de avaliar completamente o lado direito, a função C
applydefine
bloqueia o valor como uma promessa. Um objeto de promessa tem dois componentes: primeiro, algum código armazenado como um objeto de linguagem e, segundo, o ambiente no qual esse código deve ser avaliado. Fazer quase tudo com uma promessa forçará sua avaliação. Embora pareça que a atribuição complexa não esteja usando avaliação preguiçosa, na verdade está - é só que precisamos extrair o objeto de linguagem da promessa antes que ela seja avaliada.Felizmente, como SamR menciona nos comentários, é exatamente isso que
substitute
acontece se você fizer uma promessa (se estiver interessado, ele faz isso aqui ).O motivo pelo qual
enexpr
não funciona aqui é que o argumento que está sendo passado paravalue
na atribuição complexa não é o objeto de linguagem não avaliado0.2 + 0.2
, mas uma promessa que compreende tanto o objeto de linguagem0.2 + 0.2
quanto o ambiente de avaliação. Portanto, é esse objeto de promessa, e não o código bruto, que é retornado derlang:::ffi_enexpr
, a função C chamada de dentroenexpr
de. Retornar a promessa força sua avaliação, então obtemos o valor numérico 0,4 em vez do objeto de linguagem0.2 + 0.2
.