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 TRUE ou FALSE, nunca um vetor lógico com mais de um elemento, nem NA. Os operadores && e || são úteis para garantir um escalar, assim como isTRUE() e isFALSE().
  • 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 nenhum else, retorna NULL se 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”.

c(FALSE, TRUE) && TRUE
#> erro por usar vetor longo

c(FALSE) && stop()
#> FALSE pois para no `FALSE`

c(FALSE) & stop() #gera erro,
#> erro, pois não para no `FALSE`

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():

if (x == "a") {
    "option 1"
  } else if (x == "b") {
    "option 2"
  } else if (x == "c") {
    "option 3"
  } else {
    stop("`x` inválido")
}
switch(x,
  a = "option 1",
  b = "option 2",
  c = "option 3",
  stop("`x` inválido")
)

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::sum

5.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 %||% 10

També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:

out <- vector("list", length(vector))
for (i in 1:length(vector)) {
    out[[i]] <- expr(vector[[i]])
}

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:

ifelse(x < 0, -x, x)
for (i in seq_along(x)) {
  if (x[1] < 0) x[1] <- -x[1] else x[1] <- x[1]
}

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 de 1:length(x).
  • Cuidado que loops costuma remover atributos. Use seq_along() e acesse os itens você mesmo:
x <- as.Date(c("2020-01-01", "2010-01-01"))
for (i in x) print(i)

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:

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 onde tryCatch() 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.