Acho que você pode ser considerado ex-aluno sim
Resposta Curta:
Com essas funções, o que você quer é impossível (pelo menos não sem muita luta e programação). A data.table::fcase()
parece ser a implementação mais próxima do que você precisa.
Resposta Longa:
A sua pergunta é muito pertinente e já foi feita outras vezes, mas, para ficar bem claro, esse comportamento é proposital. Veja o que o Hadley fala na vignette sobre estabilidade do {vectrs}
:
Unlike ifelse()
this implies that if_else()
must always evaluate both yes
and no
in order to figure out the correct type. I think this is consistent with &&
(scalar operation, short circuits) and &
(vectorised, evaluates both sides).
Como fica claro pelas próprias palavras do Hadley, esse tipo de comportamento tem precedentes no R, mas para entender exatamente o que ele quer dizer vamos ter que aprender sobre alguns conceitos de linguagens de programação. Infelizmente vou aproveitar a sua pergunta para fazer o meu diploma valer alguma coisa…
- Execução especulativa
Execução especulativa uma técnica de otimização na qual um programa executa uma tarefa que talvez não seja necessária. Isso pode ser útil por uma série de razões apesar de parecer um desperdício! Se o seu computador consegue processar comandos em paralelo, ele pode executar a condição do if
, o resultado caso ela seja TRUE
e o resultado caso ela seja FALSE
ao mesmo tempo, permitindo uma resposta até 2x mais rápida.
Essa técnica é tão comum que aqueles famosos bugs de 2018 (Spectre e Meltdown) acontecem principalmente por causa dela.
Voltando para o if_else()
, a sua implementação de execução especulativa é diferentemente da de outras linguagens que tentam “adivinhar” se o if
vai retornar TRUE
ou FALSE
: ele usa avaliação ansiosa, ou seja, ele sempre executa os dois ramos do condicional independentemente do resultado do if
. A motivação disso é bem diferente de “otimizar” a computação (como vimos no exemplo anterior), mas sim garantir que ambos os lados da resposta vão ter o mesmo comprimento e o mesmo tipo.
Veja o código do if_else()
e perceba que nele não existe nenhum if
ou else
, ou seja, ambos os ramos do condicional necessariamente vão ser executados:
if_else <- function(condition, true, false, missing = NULL) {
if (!is.logical(condition)) {
bad_args("condition", "must be a logical vector.")
}
out <- true[rep(NA_integer_, length(condition))]
out <- replace_with(
out, condition, true,
fmt_args(~ true),
glue("length of {fmt_args(~condition)}")
)
out <- replace_with(
out, !condition, false,
fmt_args(~ false),
glue("length of {fmt_args(~condition)}")
)
out <- replace_with(
out, is.na(condition), missing,
fmt_args(~ missing),
glue("length of {fmt_args(~condition)}")
)
out
}
- Avaliação de curto-circuito
O conceito de avaliação de curto-circuito é mais simples e foi citado diretamente pelo Hadley. Ele basicamente quer dizer que, em uma operação booleana, o segundo argumento somente será executado se o valor do primeiro não for suficiente para determinar o valor da expressão (por exemplo, se temos A && B
e A
for FALSE
, não precisamos saber o valor de B
para saber que a resposta da expressão é FALSE
).
Armados desse conhecimento, podemos entender finalmente a frase do Hadley: " I think this is consistent with &&
(scalar operation, short circuits) and &
(vectorised, evaluates both sides)". A implementação do if_else()
foi feita para ser consistente com o operador &
, ou seja, vetorizada e com avaliação especulativa (ansiosa), enquanto um if-else
comum é consistente com o &&
, a saber, escalar e com avaliação de curto-circuito.
Agora vamos ver alguns exemplos para tentar entender como cada um desses operadores se comporta:
# Função que retorna TRUE
f <- function() {
warning("ANSIOSO")
TRUE
}
# Preguiçoso (só escalares)
if (TRUE) TRUE else f()
#> [1] TRUE
# Ansioso (funciona para vetores)
dplyr::if_else(TRUE, TRUE, f())
#> Warning in f(): ANSIOSO
#> [1] TRUE
# Com curto-circuito (só escalares)
FALSE && f()
#> [1] FALSE
# Sem curto-circuito (fuciona para vetores)
FALSE & f()
#> Warning in f(): ANSIOSO
#> [1] FALSE
Acho que com esses códigos fica claro que, na verdade, o ifelse()
é a exceção e não a regra! Note que aqui eu usei sempre entradas escalares (comprimento 1) por questão didática, mas estão marcados os operadores que podem receber vetores.
# Preguiçoso (funciona para vetores)
ifelse(TRUE, TRUE, f())
#> [1] TRUE
Conclui-se que, no que diz respeito à sua pergunta, não existe um jeito óbvio de fazer o if_else()
e o case_when()
trabalharem com execução preguiçosa sem mudar fundamentalmente o comportamento (e a lógica por trás) dessas funções. Se você quiser uma alternativa, aparentemente o data.table::fcase()
funciona como um case_when()
sem avaliação ansiosa.