5  Flujo de control

5.1 Introducción

Hay dos herramientas principales de flujo de control: opciones y bucles. Las opciones, como declaraciones if y llamadas switch(), le permiten ejecutar código diferente dependiendo de la entrada. Los bucles, como for y while, le permiten ejecutar código repetidamente, normalmente con opciones cambiantes. Espero que ya esté familiarizado con los conceptos básicos de estas funciones, por lo que cubriré brevemente algunos detalles técnicos y luego presentaré algunas características útiles, pero menos conocidas.

El sistema de condiciones (mensajes, advertencias y errores), del que aprenderá en el Capítulo 8, también proporciona un flujo de control no local.

Prueba

¿Quieres saltarte este capítulo? Anímate, si puedes responder las siguientes preguntas. Encuentre las respuestas al final del capítulo en la Sección 5.4.

  • ¿Cuál es la diferencia entre if y ifelse()?

  • En el siguiente código, ¿cuál será el valor de y si x es TRUE? ¿Qué pasa si x es FALSE? ¿Qué pasa si x es NA?

    y <- if (x) 3
  • ¿Qué devuelve switch("x", x = , y = 2, z = 3)?

Estructura

  • La Sección 5.2 se sumerge en los detalles de if, luego analiza los parientes cercanos ifelse() y switch().

  • La Sección 5.3 comienza recordándole la estructura básica del bucle for en R, analiza algunos errores comunes y luego habla sobre las declaraciones while y repeat relacionadas.

5.2 Opciones

La forma básica de una sentencia if en R es la siguiente:

if (condition) true_action
if (condition) true_action else false_action

Si conditionn es TRUE, se evalúa true_action; si condition es FALSE, se evalúa la false_action opcional.

Por lo general, las acciones son declaraciones compuestas contenidas dentro de {:

grade <- function(x) {
  if (x > 90) {
    "A"
  } else if (x > 80) {
    "B"
  } else if (x > 50) {
    "C"
  } else {
    "F"
  }
}

if devuelve un valor para que pueda asignar los resultados:

x1 <- if (TRUE) 1 else 2
x2 <- if (FALSE) 1 else 2

c(x1, x2)
#> [1] 1 2

(Recomiendo asignar los resultados de una declaración if solo cuando la expresión completa cabe en una línea; de lo contrario, tiende a ser difícil de leer.)

Cuando usa la forma de argumento único sin una declaración else, if invisiblemente (Sección 6.7.2) devuelve NULL si la condición es FALSE. Dado que funciones como c() y paste() descartan entradas NULL, esto permite una expresión compacta de ciertos modismos:

greet <- function(name, birthday = FALSE) {
  paste0(
    "Hi ", name,
    if (birthday) " and HAPPY BIRTHDAY"
  )
}
greet("Maria", FALSE)
#> [1] "Hi Maria"
greet("Jaime", TRUE)
#> [1] "Hi Jaime and HAPPY BIRTHDAY"

5.2.1 Entradas inválidas

La condición debe evaluarse como un solo TRUE o FALSE. La mayoría de las otras entradas generarán un error:

if ("x") 1
#> Error in if ("x") 1: argument is not interpretable as logical
if (logical()) 1
#> Error in if (logical()) 1: argument is of length zero
if (NA) 1
#> Error in if (NA) 1: missing value where TRUE/FALSE needed
if (c(TRUE, FALSE)) 1
#> Error in if (c(TRUE, FALSE)) 1: the condition has length > 1

5.2.2 if vectorizado

Dado que if solo funciona con un solo TRUE o FALSE, es posible que se pregunte qué hacer si tiene un vector de valores lógicos. Manejar vectores de valores es el trabajo de ifelse(): una función vectorizada con vectores test, y no (que se reciclarán a la misma longitud):

x <- 1:10
ifelse(x %% 5 == 0, "XXX", as.character(x))
#>  [1] "1"   "2"   "3"   "4"   "XXX" "6"   "7"   "8"   "9"   "XXX"

ifelse(x %% 2 == 0, "even", "odd")
#>  [1] "odd"  "even" "odd"  "even" "odd"  "even" "odd"  "even" "odd"  "even"

Tenga en cuenta que los valores faltantes se propagarán a la salida.

Recomiendo usar ifelse() solo cuando los vectores y no son del mismo tipo, ya que de otro modo es difícil predecir el tipo de salida. Ver https://vctrs.r-lib.org/articles/stability.html#ifelse para una discusión adicional.

Otro equivalente vectorizado es el más general dplyr::case_when(). Utiliza una sintaxis especial para permitir cualquier número de pares de vectores de condición:

dplyr::case_when(
  x %% 35 == 0 ~ "fizz buzz",
  x %% 5 == 0 ~ "fizz",
  x %% 7 == 0 ~ "buzz",
  is.na(x) ~ "???",
  TRUE ~ as.character(x)
)
#>  [1] "1"    "2"    "3"    "4"    "fizz" "6"    "buzz" "8"    "9"    "fizz"

5.2.3 declaración switch()

Estrechamente relacionada con if está la sentencia switch(). Es un equivalente compacto de propósito especial que le permite reemplazar código como:

x_option <- function(x) {
  if (x == "a") {
    "option 1"
  } else if (x == "b") {
    "option 2" 
  } else if (x == "c") {
    "option 3"
  } else {
    stop("Invalid `x` value")
  }
}

con la más sucinta:

x_option <- function(x) {
  switch(x,
    a = "option 1",
    b = "option 2",
    c = "option 3",
    stop("Invalid `x` value")
  )
}

El último componente de un switch() siempre debería arrojar un error; de lo contrario, las entradas no coincidentes devolverán invisiblemente NULL:

(switch("c", a = 1, b = 2))
#> NULL

Si varias entradas tienen la misma salida, puede dejar el lado derecho de = vacío y la entrada “caerá” al siguiente valor. Esto imita el comportamiento de la sentencia switch de C:

legs <- function(x) {
  switch(x,
    cow = ,
    horse = ,
    dog = 4,
    human = ,
    chicken = 2,
    plant = 0,
    stop("Unknown input")
  )
}
legs("cow")
#> [1] 4
legs("dog")
#> [1] 4

También es posible usar switch() con una x numérica, pero es más difícil de leer y tiene modos de falla no deseados si x no es un número entero. Recomiendo usar switch() solo con entradas de caracteres.

5.2.4 Ejercicios

  1. ¿Qué tipo de vector devuelve cada una de las siguientes llamadas a ifelse()?

    ifelse(TRUE, 1, "no")
    ifelse(FALSE, 1, "no")
    ifelse(NA, 1, "no")

    Lee la documentación y escribe las reglas con tus propias palabras.

  2. ¿Por qué funciona el siguiente código?

    x <- 1:10
    if (length(x)) "not empty" else "empty"
    #> [1] "not empty"
    
    x <- numeric()
    if (length(x)) "not empty" else "empty"
    #> [1] "empty"

5.3 Bucles

for los bucles se utilizan para iterar sobre los elementos de un vector. Tienen la siguiente forma básica:

for (item in vector) perform_action

Para cada elemento en vector, perform_action se llama una vez; actualizando el valor de item cada vez.

for (i in 1:3) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3

(Al iterar sobre un vector de índices, es convencional usar nombres de variables muy cortos como i, j, or k.)

N.B.: for asigna el item al entorno actual, sobrescribiendo cualquier variable existente con el mismo nombre:

i <- 100
for (i in 1:3) {}
i
#> [1] 3

Hay dos formas de terminar un bucle for antes de tiempo:

  • next sale de la iteración actual.
  • break sale de todo el bucle for.
for (i in 1:10) {
  if (i < 3) 
    next

  print(i)
  
  if (i >= 5)
    break
}
#> [1] 3
#> [1] 4
#> [1] 5

5.3.1 Errores comunes

Hay tres errores comunes a tener en cuenta al usar for. Primero, si está generando datos, asegúrese de asignar previamente el contenedor de salida. De lo contrario, el ciclo será muy lento; consulte las Secciones Sección 23.2.2 y Sección 24.6 para obtener más detalles. La función vector() es útil aquí.

means <- c(1, 50, 20)
out <- vector("list", length(means))
for (i in 1:length(means)) {
  out[[i]] <- rnorm(10, means[[i]])
}

A continuación, tenga cuidado con la iteración sobre 1:length(x), que fallará de manera inútil si x tiene una longitud de 0:

means <- c()
out <- vector("list", length(means))
for (i in 1:length(means)) {
  out[[i]] <- rnorm(10, means[[i]])
}
#> Error in rnorm(10, means[[i]]): invalid arguments

Esto ocurre porque : funciona tanto con secuencias crecientes como decrecientes:

1:length(means)
#> [1] 1 0

Utilice seq_along(x) en su lugar. Siempre devuelve un valor de la misma longitud que x:

seq_along(means)
#> integer(0)

out <- vector("list", length(means))
for (i in seq_along(means)) {
  out[[i]] <- rnorm(10, means[[i]])
}

Finalmente, es posible que encuentre problemas al iterar sobre los vectores de S3, ya que los bucles normalmente eliminan los atributos:

xs <- as.Date(c("2020-01-01", "2010-01-01"))
for (x in xs) {
  print(x)
}
#> [1] 18262
#> [1] 14610

Solucione esto llamando a [[ usted mismo:

for (i in seq_along(xs)) {
  print(xs[[i]])
}
#> [1] "2020-01-01"
#> [1] "2010-01-01"

5.3.2 Herramientas relacionadas

Los bucles for son útiles si conoce de antemano el conjunto de valores que desea iterar. Si no lo sabe, hay dos herramientas relacionadas con especificaciones más flexibles:

  • while(condition) action: ejecuta action mientras condition sea TRUE.

  • repeat(action): repite action siempre (i.e. hasta que encuentre break).

R no tiene un equivalente a la sintaxis do {acción} while (condition) que se encuentra en otros idiomas.

Puede reescribir cualquier bucle for para usar while en su lugar, y puede reescribir cualquier bucle while para usar repeat, pero lo contrario no es cierto. Eso significa que while es más flexible que for, y repeat es más flexible que while. Sin embargo, es una buena práctica usar la solución menos flexible a un problema, por lo que debe usar for siempre que sea posible.

En términos generales, no debería necesitar usar bucles for para tareas de análisis de datos, ya que map() y apply() ya brindan soluciones menos flexibles para la mayoría de los problemas. Aprenderá más en el Capítulo 9.

5.3.3 Ejercicios

  1. ¿Por qué este código tiene éxito sin errores ni advertencias?

    x <- numeric()
    out <- vector("list", length(x))
    for (i in 1:length(x)) {
      out[i] <- x[i] ^ 2
    }
    out
  2. Cuando se evalúa el siguiente código, ¿qué puede decir sobre el vector que se itera?

    xs <- c(1, 2, 3)
    for (x in xs) {
      xs <- c(xs, x * 2)
    }
    xs
    #> [1] 1 2 3 2 4 6
  3. ¿Qué le dice el siguiente código acerca de cuándo se actualiza el índice?

    for (i in 1:3) {
      i <- i * 2
      print(i) 
    }
    #> [1] 2
    #> [1] 4
    #> [1] 6

5.4 Respuestas de la prueba

  • if trabaja con escalares; ifelse() trabaja con vectores.

  • Cuando x es TRUE, y será 3; cuando FALSE, y será NULL; cuando NA, la declaración if arrojará un error.

  • Esta instrucción switch() hace uso de fallas, por lo que devolverá 2. Consulte los detalles en la Sección 5.2.3.