5 Control Flow e Condições
5.1 Introdução
Control flow é o termo usado para descrever as ferramentas que alteram a ordem de execução de um dado código. Nesse capítulo, brevemente apresentarei-as. Este capítulo está escrito de forma mais enxuta que os capítulos anteriores.
5.2 Escolhas
Primeiro, temos as ferramentas que permitem escolher entre diferentes caminhos de execução, dependendo de condições.
5.2.1 If e Else
No R, podemos criar uma expressão que apenas é avaliada se uma certa condição for verdade. Se não for, podemos complementar com expressões auxiliares.
Pegando o exemplo do “Advanced R”, a nota A/B/C/D pode ser calculada com base em uma nota numérica x da seguinte forma:
if (x > 90) {
"A"
} else if (x > 80) {
"B"
} else if (x > 50) {
"C"
} else {
"D"
}Algumas características gerais:
- A condição deve ser
TRUEouFALSE, nunca um vetor lógico com mais de um elemento, nemNA. Os operadores&&e||são úteis para garantir um escalar, assim comoisTRUE()eisFALSE(). - Existe uma versão enxuta, que cabe em uma linha e sem
{:if (cond) expr. - Como tudo que executa uma ação no R,
ifé uma função, e retorna um valor49. Se não houver nenhumelse, retornaNULLse a condição não for satisfeita. Exemplo:x <- if (TRUE) 1 else 2.
5.2.2 If e Else Vetorizado
A função ifelse() funciona para um vetor de condições, diferentemente de if, que exigia um único valor booleano. Para cada elemento, ela checa a condição, e avalia uma ou outra expressão, salvando os resultados em um vetor.
O que o código abaixo vai retornar?
x <- 1:10
ifelse(x > 5, NA, x)Outra opção é dplyr::case_when(), que permite vários pares condição-resultado:
k <- 1:10
dplyr::case_when(
k %% 35 == 0 ~ "fizz buzz",
k %% 5 == 0 ~ "fizz",
k %% 7 == 0 ~ "buzz",
is.na(k) ~ "???",
TRUE ~ as.character(k)
)
#> [1] "1" "2" "3" "4" "fizz" "6" "buzz" "8" "9" "fizz"ifelse() é inconsistente quando os valores de retorno tem diferentes tipos. Prefira utilizar funs::if_else().
5.2.3 And And e Or Or
Existem os operadores “and and” && e “or or” || que garantem sempre um único valor booleano (dão erros com vetores longos), além de serem “short circuit”.
Por isso, use && e || em if, & e | em ifelse().
5.2.4 Switch
Existe uma maneira mais simples de escrever uma expressão if de “escolha de opções”, com a função switch():
5.2.5 Match
De maneira mais geral do que switch, que associava um resultado para cada valor de uma string x, podemos associar um resultado para cada ‘padrão’ que um x de qualquer tipo possa assumir.
As funções predicater::match_hash() e predicater::match_id() fazem exatamente isso.
match_hash(1.42, # x de qualquer tipo
1.42 ~ base::sum,
"oi" ~ -Inf
) #> base::sum5.2.6 Infix Operator
Muitas vezes, queremos substituir o valor de uma variável caso ela seja NULL. Muitas vezes, funções usam isso para definir argumentos padrão.
Para isso, temos o infix operator:
k <- NULL
k <- k %||% 10Também temos o oposto em rlang::`%&&%`, que retorna um valor para o caso de x não ser NULL.
5.3 Repetições
Loops são utilizados para rodar um mesmo código várias vezes, possivelmente variando os inputs que ele recebe.
5.3.1 Repeat e break
O loop mais simples é o repeat, que repete uma ação pra sempre até encontrar uma keyword especial: break:
repeat {
expr
if (condition) break
}5.3.2 While
O loop while é similar, mas repete a ação enquanto uma condição for verdadeira:
while (condition) {
expr
}Um loop while (TRUE) ... é equivalente a um repeat.
5.3.3 For e next
O loop for repete um código para cada elemento de uma coleção:
for (item in vector) {
expr
}Esse loop cria a variável item, cujo valor vai sendo atualizado a cada iteração, e fica armazenada no ambiente global. Normalmente, expr é uma expressão que depende do valor de item.
As três opções de loop podem ‘pular’ uma iteração com a keyword next.
Como será a ordem do loop abaixo?
for (i in 1:10) {
if (i < 3) next
print(i)
if (i >= 5) break
}5.3.4 Map
O map é uma versão mais restritiva de for, onde expr é uma função que recebe item como argumento, e retorna um valor. O map retorna um vetor com os valores retornados por cada chamada de expr. A função lapply() our purrr::map(), chamada como lapply(vector, expr), executa algo como:
O map sempre executa todas as iterações, não permitindo break ou next. Além disso, todas as iterações geram um valor para o vetor de saída, o que não precisa acontecer nos outros loops.
As funções de map no R oferecem opções de como simplificar seu resultado. lapply() sempre retorna uma lista, vapply() permite que você defina o tipo de saída. O pacote purrr oferece funções mais consistentes como map() e map_dbl(). Mais sobre maps será discutido no capítulo de programação funcional.
5.3.5 Operações vetorizadas
O R é uma lingua vetorizada, use operações vetorizadas no lugar de loops:
5.3.6 Comparando os loops
Para entender as diferenças entre os loops, e quando usar cada um, pense-os como opções mais flexíveis ou mais restritivas. A tabela abaixo descreve se os loops permitem loops infinitos, next e break, se cada iteração necessariamente gera um resultado, e se ele consegue afetar o ambiente global:
| Loop | Finito |
next e break
|
omitir resultados | Afetar o ambiente global |
|---|---|---|---|---|
repeat / while (TRUE)
|
via break
|
sim | sim | sim |
while |
via break ou condição |
sim | sim | sim |
for |
sempre | sim | sim | sim |
| map | sempre | não | não | via <<- ou assign()
|
| operação vetorizada | sempre | não | não | depende |
Considere a máxima de que, quando não se precisa de flexibilidade, a opção mais restritiva/especializada deixa a intenção do código mais clara, abre mais portas para otimizações, e menos brechas para erros. Veja se é possível usar uma operação vetorizada, se não, considere um map, se não, considere um for, e assim por diante.
Dicas gerais:
- Ao usar um loop para criar uma variável, crie o “contêiner” de antemão, pois é bem mais rápido:
x <- NULL; for(i in 1:5) x <- c(x, i)
x <- NULL; for(i in 1:5) x[[i]] <- i- Prefira usar
seq_along(x)no lugar de1:length(x). - Cuidado que loops costuma remover atributos. Use
seq_along()e acesse os itens você mesmo:
5.4 Condições
O sistema de condições serve para funções indicarem ao usuário alguma informação, como a existência de erros.
Existem três tipos de informação:
- Erros: muito severos, indicam que nem foi possível terminar a execução da função
- Avisos: médiamente severos, indicam alga provávelmente errado, mas que não impediu a execução.
- Mensagens: pouco severos, normalmente indicam que a função realizou alguma correção para o usuário.
5.4.1 Signaling
Autores de funções devem aprender quando e como indicar cada tipo de condição. Se tiver curiosidade, veja a seção 8.2 do AdvR.
Dentro da função, chamadas à stop(), warning(), ou message() criam a condição. stop(), inclusive, para a execução da função. Também existe rlang::abort(), rlang::warn(), rlang::inform().
Considere as função de teste do pacote mtsdesc.
test$category <- function(arg, options, env) {
arg_name <- rlang::ensym(arg)
if (!all(arg %in% options)) {
cli_abort(
"{.var {arg_name}} must contain only {.or {options}}",
call = env
)
}
}
test$unused <- function(arg, x, class, env) {
arg_name <- ensym(arg)
if (class(x) == class) {
cli_inform(
"{.val {arg_name}} is unused if `x` is of class {class}.",
call = env
)
}
}5.4.2 Ignoring
No R, podemos ignorar condições:
- Ignore erros com
try(). - Ignore avisos com
suppressWarnings(). - Ignore mensagens com
suppressMessages().
Considere o padrão abaixo.
x <- NULL
try(x <- call_que_pode_dar_errado())5.4.3 Handling
Podemos lidar com condições usando tryCatch() e withCallingHandlers(), que especificam funções para se executar no caso de cada tipo de condição:
tryCatch(
error = function(cnd) {
# código a ser executado quando ocorrer um erro
},
warning = function(cnd) {
# código a ser executado quando um aviso for sinalizado
},
message = function(cnd) {
# código a ser executado quando uma mensagem for sinalizada
},
call_rodada_com_handlers
)Qual a diferença das duas?
-
tryCatch()define exiting handlers, após a condição aparecer, a função handler relevante é ativada, e seu valor é retornado para ondetryCatch()foi chamado. -
withCallingHandlers()define calling handlers, após a condição, a função handler relevante é ativada, e seu valor é retornado para onde a call que gerou a condição estava.
Isto é, uma sai da call, outra não. A primeira é mais intuitiva para erros, a segunda para avisos ou mensagens.
Complemento
Dicionário de Funções
| Categoria | Função | Descrição |
|---|---|---|
| control flow |
if, switch(), ifelse(), %||%
|
if else, ifelse vetorizado, e infix operator |
| control flow |
for, while, repeat
|
loops |
| control flow |
break, next
|
keywords |
| operadores |
&&, ||
|
lógica |
| outros |
seq(), seq.int(), seq_along(), seq_len()
|
geração de sequências |
| maps |
lapply(), apply(), purrr::map()
|
maps |
| maps |
sapply(), replicate(), Map()
|
maps (variações) |
| conditions |
stop(), warning(), message()
|
signaling conditions |
| conditions |
rlang::abort(), rlang::warn(), rlang::inform()
|
signaling conditions (rlang) |
| conditions |
try(), supressWarnings(), supressErrors()
|
ignoring conditions |
| conditions |
tryCatch(), withCallingHandlers
|
handling conditions |
Referências
Referência básica: Advanced R (2e) cap. 5.