9  Funciones

9.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

9.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 escribiste.

En estas situaciones es una buena idea comenzar a encapsular código en funciones. En esta sección veremos cómo 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 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 gráficos o tablas, más bien son definiciones de funciones secundarias u otras herramientas, no tiene tanto sentido usar archivos .Rmd o .qmd. En estos casos podemos volver a los tradicionales scripts (.R).

9.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, que son los elementos de entrada que necesita el código para correr, en el ejemplo arg1, arg2, ....
  • Colocás el código que has desarrollado en el cuerpo de la función, que se identifica como un bloque entre {} inmediatamente después de function(...).

Imaginemos que trabajamos en una carpinteria y vendemos piezas de maderas precortadas para armar muebles. Algunos de los planos de los muebles vienen en pulgadas, pero en Argentina se trabaja en centimetros. Tenemos este código que nos permite transformar las medidas:

largo_tabla_en_pulgadas <- 12 # En pulgadas!!

largo_tabla_cm <- largo_tabla_en_pulgadas * 2.54 # convierto a centímetros

Este código no va a ser útil si querés usarlo en otros lugares, por ejemplo si queremos calcular el ancho o queremos convertir la medida de otra pieza que no sea una tabla. Entonces necesitamos generalizarlo en una función.

El primer paso es pensar un nombre, por ejemplo pulgadas_a_centimetros. Luego tenemos que analizar el código e identificar cuáles son los argumentos, la información que necesita la función, en este caso el largo en pulgadas, ese es un argumento.

pulgadas_a_centimetros <- function(largo_en_pulgadas) {
  largo_en_pulgadas * 2.54
}

Esta función está lista para usar, por ejemplo podemos convertir 20 pulgadas a centímetros:

pulgadas_a_centimetros(20)
[1] 50.8

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

pulgadas_a_centimetros <- function(medida_pulgadas) {
  medida_pulgadas * 2.54
}

Es posible que te encuentres con algo del estilo:

pulgadas_a_centimetros <- function(medida_pulgadas) {
  medida_centimetros <- medida_pulgadas * 2.54
  return(medida_centimetros)
}

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 buscás la variable medida_centimetros 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 cuál es su propósito.
  • Qué argumentos requiere y de qué tipo de datos son.
  • Qué genera como resultado.
  1. Creá un archivo .R con las siguientes funciones:
  • Una función que convierta longitudes de centímetros a pulgadas.
  • 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í qué hace la función, qué argumentos requiere y qué genera.

Ahora, si quisiéramos usar esa función en el análisis, podemos “cargarla” con source("nombre_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 pulgadas_a_centimetros() 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.

9.4 Testeá tu función

Es importante que pruebes tu función de distintas maneras. Primero, con algún valor para el que sabés el resultado, por ejemplo 1 pulgada son 2.54 centímetros.

pulgadas_a_centimetros(1)
[1] 2.54

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

pulgadas_a_centimetros(7.5)
[1] 19.05
pulgadas_a_centimetros(c(0, 1, 12))
[1]  0.00  2.54 30.48

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

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

pulgadas_a_centimetros("100")
Error in medida_pulgadas * 2.54: non-numeric argument to binary operator
pulgadas_a_centimetros(TRUE)
[1] 2.54

El primer 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.

9.4.1 Revisá que los argumentos sean válidos

Para revisar que los argumentos sean válidos, podemos usar if y else. En el caso de que el argumento no sea válido, podemos devolver un mensaje de error que le indique a quien usa la función que fue lo que falló y posiblemente como podria solucionar el error.

Para lo que sigue vamos a necesitar usar esquemas de flujo, if, else, etc. Revisemos cómo 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.

El paquete cli nos permite escribir mensajes de error y warnings (advertencias en inglés) más claras e informativos. En el caso de que la función reciba un argumento que no es numérico, podemos usar cli::cli_abort() para generar un mensaje de error más claro:

pulgadas_a_centimetros <- function(medida_pulgadas) {
  
  if (!is.numeric(medida_pulgadas)) {
    cli::cli_abort(c(
      "medida_pulgadas debe ser numérico.",
      "i" =  "La variable ingresada es un {class(medida_pulgadas)[1]}."
    ))
  }
  
  medida_pulgadas * 2.54
}
pulgadas_a_centimetros("100")
Error in `pulgadas_a_centimetros()`:
! medida_pulgadas 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 circunstancia, por ejemplo si queremos mostrar un warning, si la función corrió con éxito, 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.

9.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()

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