8  Funciones

8.1 Objetivos de aprendizaje

  • Convertir un bloque de código R en una función
  • Utilizar funciones de otros paquetes en una función de su propio paquete
  • Devolver un mensaje de error desde una función

8.2 Trabajando con funciones y otro código

Hasta ahora tenemos un proyecto con una determinada estructura de carpetas que nos permite ordenar nuestro trabajo. Inicialmente escribir el código necesario para resolver un problema, leer un archivo o calcular medidas estadísticas es lo más razonable. Pero es posible que te encuentres repitiendo las mismas lineas de código o copiado y pegando código de un lado para el otro porque necesitás reutilizar algo que ya escribíste.

En estas situaciones es una buena idea comenzar a encapsular código en funciones. En esta sección veremos como escribir funciones, más adelante empaquetaremos esas funciones en un paquete.

Escribir funciones tiene cuatro grandes ventajas sobre copiar y pegar código:

  • Podés nombrar tus funciones con un nombre descriptivo que facilite la comprensión del código.
  • Si por alguna razón tenés que cambiar el código, sólo es necesitás hacerlo en un único lugar.
  • Eliminás la posibilidad de cometer errores al copiar y pegar código.
  • Podés reutilizar el código en las funciones en otros proyectos.

En las situaciones donde nuestro código no produce salidas como graficos o tablas, más bien son definiciones de funciones secundarias u otras herramientras, no tiene tanto sentido usar archivos .Rmd o .qmd. En estos casos podemos volver a los tradicionales scripts (.R).

8.3 Esqueleto de una función

Cualquier función en R tendrá la siguiente pinta (se le llama firma de una función):

nombre_de_funcion <- function(arg1, arg2, ...) {
# código que hace algo 
}

Como habrás notado necesitamos la función function() para crear una función. Hay tres pasos clave para crear una nueva función:

  • Tenés que elegir un nombre para la función.
  • Enumeras los argumentos, los elementos de entrada que necesita el código para correr, en el ejemplo arg1, arg2, ....
  • Colocas el código que has desarrollado en el cuerpo de la función, un bloque entre {} inmediatamente después de function(...).

Imaginemos que tenemos este código:

temperatura_ayer <- 53.6 # en Fahrenheit!

temperatura_ayer_c <- (temperatura_ayer - 32) * 5/9 # convierto a centigrados

De nuevo, este código no va a ser útil si querés usarlo en otro lugares, necesitamos generalizarlo en una función.

El primer paso es pensar un nombre, por ejemplo fahrenheit_a_centigrados. Luego tenemos que analizar el código e identificar cuales son los argumentos, la información que necesita la función, en este caso la temperatura en Fahrenheit, es es un argumento.

fahrenheit_a_centigrados <- function(temperatura_ayer) {
  (temperatura_ayer - 32) * 5/9
}

Está función está lista para usar, por ejemplo podemos convertir 100 Fahrenheit a centigrados:

fahrenheit_a_centigrados(100)
[1] 37.77778

Si bien podemos nombrar a los argumentos de las funciones de cualquier manera, temperatura_ayer no es razonable para una función general. Un mejor nombre podría ser temperatura_fahrenheit

fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
  (temperatura_fahrenheit - 32) * 5/9
}

Es posible que te encuentres con algo del estilo:

fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
  temperatura_centigrados <- (temperatura_fahrenheit - 32) * 5/9
  return(temperatura_centigrados)
}

Usar la función return() no es necesario, R devuelve el último elemento con o sin el return() presente. Sin embargo, puede ayudar a la lectura del código cuando la función es más compleja.

Cada vez que se ejecuta una función se crea un nuevo entorno, desde cero, propio de la función para contener su ejecución. Notá que si buscas la variable temperatura_centigrados en el Environment no la vas a encontrar. Eso es así porque todo lo que ocurre adentro de la función, se queda adentro de la función. El código corre en ese “entorno” independiente del entorno general.

Aquí podemos hacer un paréntesis para mencionar la necesidad de documentar apropiadamente cualquier función o código que generemos. Para una función deberíamos incluir:

  • Qué hace o cual es su propósito.

  • Qué argumentos requiere y de que tipo de datos son.

  • Qué genera cómo resultado.

  1. Creá un archivo .R con las siguientes funciones:
  • Una función que convierta la temperatura de centigrados a fahrenheit.
  • Una función utilizando el siguiente código mean(is.na(x)). ¿Qué resultado da este código? Probalo con x <- c(0, 1, 2, NA, 4, NA).
  • Una función utilizando el siguiente código x / sum(x, na.rm = TRUE). ¿Qué resultado da este código? Probalo con x <- c(1:5).
  1. Guardá el archivo .R con un nombre informativo.
  2. Al comienzo del archivo, en comentarios, antes de cada función, incluí que hace la función, que argumentos requiere y que genera.

Ahora si quisieramos usar esa función en el análisis, podemos “cargarla” con source("archivo.R").

La escritura de una función debería arrancar con un código que ya funciona. Es decir, en lugar de empezar desde cero fahrenheit_a_centigrados() usamos el código que ya tenemos y que funciona para un ejemplo particular. Luego cambiando el nombre del argumento podemos generalizar el código.

8.4 Testeá tu función

Es importante que pruebes tu función de distintas maneras. Primero, con algún valor para el que sabes el resultado, por ejemplo 32 fahrenheit es 0º centigrados.

fahrenheit_a_centigrados(32)
[1] 0

También es importante testear la función con datos diferentes, por ejemplo en vez de 1 número entero, podemos usar un real o un vector de números.

fahrenheit_a_centigrados(25.5)
[1] -3.611111
fahrenheit_a_centigrados(c(0, 32, 100))
[1] -17.77778   0.00000  37.77778

Hay que chequear si estos resultados son correctos. ¿Esperabas obtener 3 valores en el segundo último ejemplo? ¿tiene sentido que haga esto?

Finalmente, tenemos que probar la función con otras cosas.

fahrenheit_a_centigrados("100")
Error in temperatura_fahrenheit - 32: non-numeric argument to binary operator
fahrenheit_a_centigrados(TRUE)
[1] -17.22222

El primero ejemplo da un error, poco informativo pero un error al fin. El segundo ejemplo devuelve un resultado, pero ¿no debería dar error?

Por estas situaciones es importante chequear que lo que ingresa a la función es lo esperado, antes de hacer ninguna otra operación.

8.4.1 Revisá que los argumentos sean válidos

El primer acercamiento a esto es la función stopifnot()

fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
  
  stopifnot(is.numeric(temperatura_fahrenheit))
  
  (temperatura_fahrenheit - 32) * 5/9
}

Ahora si volvemos a correr algunos de los ejemplos previos, obtendremos:

fahrenheit_a_centigrados("100")
Error in fahrenheit_a_centigrados("100"): is.numeric(temperatura_fahrenheit) is not TRUE

Pero de nuevo, esta función no devuelve un mensaje muy informativo.

Para lo que sigue vamos a necesitar usar esquemas de flujo, if, else, etc. Revisemos como es la sintaxis en R.

if (condición) {
# código que se ejecuta cuando la condición es TRUE
} else {
# código que se ejecuta cuando la condición es FALSE
}

En R la condición que evaluamos va entre () y usamos {} para separar cada rama de nuestro código, no es necesario indentar las líneas de código (aunque ayuda en la lectura!).

Además de if y else, podemos usar else if cuando queremos probar distintas condiciones.

La siguiente solución implica encapsular la función stop() en una estructura if para poder incluir un mensaje de error apropiado:

fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
  
  if (!is.numeric(temperatura_fahrenheit)) {
    stop("temperatura_centigrados debe ser numérico,\n",
         "La variable ingresada es un ", class(temperatura_fahrenheit)[1])
  }
  
  (temperatura_centigrados - 32) * 5/9
}
fahrenheit_a_centigrados("100")
Error in fahrenheit_a_centigrados("100"): temperatura_centigrados debe ser numérico,
La variable ingresada es un character

Este mensaje nos da mucha más información:

  • En que función ocurre el error
  • La causa del error y como resolverlo.

Pero hay otras soluciones aún más superadoras, podemos escribir mensajes de error y warnings (advertencias en inglés) usando el paquete cli:

fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
  
  if (!is.numeric(temperatura_fahrenheit)) {
    cli::cli_abort(c(
      "temperatura_fahrenheit debe ser numérico.",
      "i" =  "La variable ingresada es un {class(temperatura_fahrenheit)[1]}."
    ))
  }
  
  (temperatura_fahrenheit - 32) * 5/9
}
fahrenheit_a_centigrados("100")
Error in `fahrenheit_a_centigrados()`:
! temperatura_fahrenheit debe ser numérico.
ℹ La variable ingresada es un character.

El mensaje de error se ve mejor y nos permite organizar la información. En este caso usamos cli_abort() pero hay toda una familia de funciones según la circusntancia, por ejemplo si queremos mostrar un warning, si la función corrió con exito, etc.

Además podemos mostrar distintos tipos de mensajes:

cli::cli_bullets(c(
  "noindent",
  " " = "indent",
  "*" = "bullet",
  ">" = "arrow",
  "v" = "success",
  "x" = "danger",
  "!" = "warning",
  "i" = "info"
))
noindent
  indent
• bullet
→ arrow
✔ success
✖ danger
! warning
ℹ info

Escribí una función para descargar y leer los datos de pingüinos.

  1. La función debe aceptar un argumento, la ruta al archivo en tu computadora.
  2. Revisá si el archivo ya existe en esa ruta, usá la función file.exist().
  • Si el archivo no está descargado, el código debe descargarlo y luego leerlo.
  • Si el archivo ya está descargado, el código debe leerlo.
  1. Agrega mensajes con cli_inform() para que el usuario sepa lo que la función hizo.

8.5 Escribí funciones para humanos y computadoras

Es importante recordar que las funciones no son sólo para que las entienda R, sino también para las personas que las van a usar. A R no le importa cómo se llama tu función, o qué comentarios contiene, pero éstos son importantes para que vos y otras personas entiendan lo que hace.

El nombre de una función es importante. Lo ideal es que el nombre de tu función sea corto, pero que describa claramente lo que hace la función. Eso es difícil. Pero es mejor que el nombre sea claro a que sea muy corto. También que ayude a RStudio a autocompletar.

Generalmente, los nombres de las funciones son verbos, y los argumentos sustantivos.Esto es porque las funciones hacen algo, tienen una acción asociada. Por supuesto, hay excepciones a la regla. Los sustantivos como nombre de funciones están bien si la función calcula un sustantivo muy conocido (por ejemplo, promedio() es mejor que calcula_promedio()), o accede a alguna propiedad de un objeto (por ejemplo, coef() es mejor que extraer_coeficientes()). Una buena señal de que un sustantivo puede ser una mejor opción es si se utiliza un verbo muy amplio como «obtener», «computar», «calcular» o «determinar». Usa tu mejor criterio y no tengas miedo de cambiar el nombre de una función si más adelante se te ocurre uno mejor.

Muy corto

  • f()

No es un verbo y no es descriptivo

  • funcion_hermosa()

Nombre largo pero claro

  • computa_faltantes()
  • colapsa_anios()

Si el nombre de tu función está compuesto por varias palabras, te recomendamos utlizar «snake_case», donde cada palabra en minúscula está separada por un guión bajo. camelCase es otra buena alternativa (es mas amigable con lectores de pantallas). Lo importante es ser coherente: elegí una y no cambies.

Para ayudar a RStudio a autocompletar el nombre de las funciones es mejor esto:

  • input_select()
  • input_checkbox()
  • input_text()

que esto:

  • select_input()
  • checkbox_input()
  • text_input()

8.5.1 Construyendo un paquete de R paso a paso

  1. Crea una función para descagar y leer los datos de estaciones. Importante:
  • Usá como base la función que creamos para leer los datos de pinguinos.
  • La función deberá recibir 2 argumentos, el id de la estación (por ejemplo “NH0437”) y la ruta donde se guardará el archivo (por ejemplo “datos/NH0437.csv”)
  • Debe poder descargar y leer los datos de cualquier estación.
  1. Crea una función que se llame tabla_resumen_temperatura y que devuelva una tabla de resumen de la temperatura_abrigo_150cm para una o más estaciones. Usá el código que generaste en el ejercicio 1 de tidyr.

  2. Generá una función grafico_temperatura_mensual que devuelva un gráfico que muestre el promedio mensual de la temperatura de abrigo. usá el código que generaste para el ejercicio 2 de ggplot2. La función debe:

  • Recibir el data.frame con los datos (que pueden ser de 1 o más estaciones).
  • Tener un argumento para indicarle que colores debe usar para el gráfico.
  • Tener un argumento para definir el título del gráfico. Por defecto, si no se define el título deberá aparecer “Temperatura”

Desafio extra (opcional): modificá la función para que si no le pasas los colores necesarios, elija colores de manera aleatoria. Pista: revisá colors()