Cuaderno de ejercicios

Todos los ejercicios y desafíos del taller en una sola página

Para ver las soluciones hacé clic en Solución. Te recomendamos mirarla solo después de intentarlo. Si te trabás, poné tu nota adhesiva visible, asi te podemos ayudar.

1. ¿Qué es un paquete?

Desafío 1.1: ¿Qué hay en un paquete? 5 minutos, en el documento colaborativo

¿Cuál de estas afirmaciones sobre los paquetes de R es correcta?

  1. Un paquete solo puede tener funciones.
  2. Para crear un paquete hay que publicarlo en CRAN.
  3. Un paquete es una carpeta organizada de una manera específica, que puede tener código, documentación y datos.
  4. Para crear un paquete hay que saber programar en C.

Escribí tu respuesta (la letra) en el documento colaborativo.

La respuesta correcta es la c.

  • Si elegiste la a: los paquetes también pueden tener datos, documentación, tests, plantillas, aplicaciones Shiny…
  • Si elegiste la b: CRAN es solo uno de los lugares donde se pueden publicar paquetes. Un paquete puede vivir en tu computadora, en GitHub o en R-universe, y sigue siendo un paquete.
  • Si elegiste la d: algunos paquetes usan C o C++ para ser más rápidos, pero la gran mayoría está escrita solo en R.

Desafío 1.2: Tu código repetido 3 minutos

Pensá en algún código que hayas copiado y pegado en más de un proyecto: una función, una limpieza de datos, un gráfico con tus colores favoritos, unos datos que siempre usás.

Escribilo en una línea en el documento colaborativo. ¡Ese puede ser tu primer paquete!

2. Crear el paquete

Desafío 2.1: Crear tu paquete 10 minutos

  1. Comprobá el nombre con pak::pkg_name_check("dichosos").
  2. Creá el paquete con usethis::create_package(). Guardalo en una carpeta que encuentres fácil, por ejemplo en Documentos o en tu carpeta de proyectos, pero no adentro de otro proyecto de RStudio.
  3. En la nueva sesión de RStudio, corré devtools::check().
  4. Agregá la licencia MIT y volvé a correr devtools::check() hasta ver 0 errores y 0 warnings.
pak::pkg_name_check("dichosos")
usethis::create_package("~/Documents/dichosos")   # o la ruta que elijas

# En la sesión nueva, la del paquete:
devtools::check()
usethis::use_mit_license("Tu Nombre")
devtools::check()

Si te aparece una NOTE que dice unable to verify current time, no te preocupes: pasa cuando no hay conexión a internet o la red es lenta.

Desafío 2.2: ¿Quién es quién? 5 minutos, en parejas

Uní cada archivo o carpeta con lo que hace.

Archivo o carpeta Qué hace
1. DESCRIPTION a. Lista las funciones que el paquete exporta. No se edita a mano.
2. R/ b. Lista archivos que R ignora cuando construye el paquete.
3. NAMESPACE c. Tiene los metadatos: nombre, título, autoría, licencia, dependencias.
4. .Rbuildignore d. Guarda los scripts con las funciones.
5. dichosos.Rproj e. Es el archivo del proyecto de RStudio.

1-c, 2-d, 3-a, 4-b, 5-e.

Desafío 2.3: Presentá tu paquete 5 minutos

Abrí el archivo DESCRIPTION y completá:

  • Title: una línea, con mayúscula en cada palabra importante y sin punto final.
  • Description: un párrafo que explique qué hace el paquete.
  • Authors@R: tu nombre y tu correo.

Corré devtools::check() otra vez. ¿Cambió algo?

Package: dichosos
Title: Dichos y Expresiones Populares de America Latina
Version: 0.0.0.9000
Authors@R:
    person("Yanina", "Bellini Saibene", , "tu.correo@ejemplo.com",
           role = c("aut", "cre"))
Description: Un paquete de ejemplo para aprender a desarrollar paquetes
    de R. Incluye dichos y expresiones populares de America Latina y
    funciones para elegirlos al azar y buscarlos por palabra.
License: MIT + file LICENSE

check() debería seguir dando 0 errores y 0 warnings. Estos campos no se comprueban por su contenido, pero son lo primero que ve cualquier persona que mira tu paquete.

3. Nuestra primera función

Desafío 3.1: Tu primera función en un paquete 8 minutos

  1. Creá el archivo de la función con usethis::use_r("dicho_al_azar").
  2. Escribí la función dicho_al_azar() (podés copiarla de los snippets).
  3. Agregá al vector un dicho de tu país o de algún otro lado pero que te guste.
  4. Guardá, cargá el paquete con devtools::load_all() y llamá a la función varias veces.
usethis::use_r("dicho_al_azar")

En R/dicho_al_azar.R:

dicho_al_azar <- function() {
  dichos <- c(
    "Estar al horno",
    "Perdido como perro en cancha de bochas",
    "Pasarlo chancho",
    "Un dicho tuyo"          # <- ¡el tuyo!
  )
  sample(dichos, size = 1)
}

En la consola:

devtools::load_all()
dicho_al_azar()
dicho_al_azar()

Desafío 3.2: Poné los pasos en orden 3 minutos

Estos son los pasos para agregar una función nueva a un paquete, pero están desordenados. Ordenalos.

  • devtools::load_all()
  • Escribir la función en el archivo.
  • usethis::use_r("nombre_funcion")
  • Probar la función en la consola.
  • Guardar el archivo.
  1. usethis::use_r("nombre_funcion")
  2. Escribir la función en el archivo.
  3. Guardar el archivo.
  4. devtools::load_all()
  5. Probar la función en la consola.

Y si algo no anda: volver al paso 2. ¡Es un ciclo!

Desafío 3.3: ¿Qué va a pasar? 4 minutos

Agregá otro dicho al vector y guardá el archivo, pero no corras load_all(). Antes de llamar a dicho_al_azar() muchas veces, pensá: ¿va a aparecer el dicho nuevo?

Probalo. Después corré load_all() y volvé a probar.

Sin load_all(), R sigue usando la versión de la función que cargó la última vez: el dicho nuevo no aparece nunca. Guardar el archivo no alcanza; hay que volver a cargar el paquete. Este es uno de los errores más comunes cuando empezamos a desarrollar paquetes: “¡cambié el código y no pasa nada!”.

4. Documentar funciones

Desafío 4.1: Documentá dicho_al_azar() 8 minutos

  1. Poné el cursor adentro de la función e insertá el esqueleto de roxygen2 (Code > Insert Roxygen Skeleton).
  2. Completá el título, la descripción, @return y @examples.
  3. Corré devtools::document() y mirá la ayuda con ?dicho_al_azar.
  4. Abrí el archivo NAMESPACE. ¿Qué cambió?
#' Elegir un dicho al azar
#'
#' Devuelve un dicho popular de América Latina elegido al azar.
#'
#' @return Un texto (vector de caracteres) con el dicho.
#' @export
#'
#' @examples
#' dicho_al_azar()
dicho_al_azar <- function() {
  # ...
}
devtools::document()
?dicho_al_azar

NAMESPACE ahora tiene la línea export(dicho_al_azar), gracias a la etiqueta @export.

Desafío 4.2: Encontrá los errores 5 minutos, en parejas

Esta documentación tiene tres errores. ¿Cuáles son?

#' Elegir un dicho al azar
#
#' Devuelve un dicho popular elegido al azar.
#'
#' @params pais El país del que queremos el dicho.
#' @return Un texto con el dicho.
#'
#' @examples
#' dicho_al_azar()
dicho_al_azar <- function(pais = NULL) {
  # ...
}
  1. La segunda línea empieza con # en lugar de #'. Para roxygen2, ahí termina el bloque.
  2. La etiqueta es @param, en singular, no @params.
  3. Falta @export. Sin esa etiqueta, la función no se exporta: no se puede usar con library(dichosos) después de instalar el paquete.

Desafío 4.3 (si te sobra tiempo o para hacerlo en casa): La ayuda por dentro 2 minutos

Abrí la carpeta man/ y el archivo dicho_al_azar.Rd. ¿Lo escribiste vos? ¿Lo deberías editar?

Lo escribió roxygen2, a partir de tus comentarios. La primera línea dice % Generated by roxygen2: do not edit by hand. Si lo editás a mano, la próxima vez que corras document() se pierden los cambios. Siempre se edita el comentario #' en el archivo .R.

5. Datos en el paquete

Desafío 5.1: Datos adentro del paquete 10 minutos

  1. Corré usethis::use_data_raw("dichos").
  2. Descargá dichos.csv y guardalo en la carpeta data-raw/ de tu paquete.
  3. Completá data-raw/dichos.R para leer el CSV y guardarlo con usethis::use_data(). Corré el script.
  4. Corré devtools::load_all() y mirá los datos: head(dichos), table(dichos$pais).
usethis::use_data_raw("dichos")
download.file(
  "https://yabellini.github.io/desarrollo_paquetes/datos/dichos.csv",
  "data-raw/dichos.csv"
)

En data-raw/dichos.R:

dichos <- read.csv("data-raw/dichos.csv", encoding = "UTF-8")
usethis::use_data(dichos, overwrite = TRUE)
source("data-raw/dichos.R")
devtools::load_all()
head(dichos)
table(dichos$pais)

Desafío 5.2: Completá los espacios 7 minutos

Esta nueva versión de dicho_al_azar() usa los datos y tiene un argumento pais. Completá los ____, guardá, cargá y probá con dicho_al_azar(pais = "Chile").

#' Elegir un dicho al azar
#'
#' Devuelve un dicho elegido al azar del conjunto de datos [dichos].
#'
#' @param ____ Texto con el nombre de un país, por ejemplo `"Argentina"`.
#'   Si es `NULL` (el valor por defecto) se elige entre todos los países.
#'
#' @return Un texto (vector de caracteres de largo 1) con el dicho.
#' @export
#'
#' @examples
#' dicho_al_azar()
#' dicho_al_azar(pais = "Argentina")
dicho_al_azar <- function(pais = ____) {
  opciones <- dichos

  if (!is.null(pais)) {
    opciones <- dichos[dichos$____ == pais, ]
  }

  fila <- sample(nrow(____), size = 1)
  opciones$____[fila]
}
#' @param pais Texto con el nombre de un país, ...
dicho_al_azar <- function(pais = NULL) {
  opciones <- dichos

  if (!is.null(pais)) {
    opciones <- dichos[dichos$pais == pais, ]
  }

  fila <- sample(nrow(opciones), size = 1)
  opciones$dicho[fila]
}

No te olvides de devtools::document(): cambiamos la documentación.

Desafío 5.3: ¿Y si el país no existe? 3 minutos

Probá dicho_al_azar(pais = "Narnia"). ¿Qué devuelve? ¿Es útil para quien usa el paquete? Escribí en el documento colaborativo qué te gustaría que pase.

Devuelve character(0): un vector de texto vacío, sin ningún aviso. La función “falla en silencio”, y quien la usa no sabe qué hizo mal. Lo vamos a arreglar en el próximo episodio con un mensaje de error claro.

Desafío 5.4 (para llevar a casa): Sumá un dicho de tu país 5 minutos

  1. Agregá una fila a data-raw/dichos.csv con un dicho de tu país.
  2. Volvé a correr data-raw/dichos.R.
  3. Si agregaste un país nuevo, actualizá el número de filas en la documentación de R/data.R.
  4. Corré devtools::load_all() y buscá tu dicho.

Compartilo en el documento colaborativo: ¡juntamos todos los dichos del taller!

6. Dependencias y mensajes claros

Desafío 6.1: ¿Imports o Suggests? 3 minutos

Para cada caso, ¿la dependencia va en Imports o en Suggests?

  1. Tu función usa cli::cli_abort() para avisar errores.
  2. Usás testthat para los tests del paquete.
  3. Usás rmarkdown solo para construir el README.
  4. Tu función principal filtra datos con dplyr::filter().
  1. Imports: sin cli, la función no puede funcionar.
  2. Suggests: los tests solo los corre quien desarrolla el paquete.
  3. Suggests (o ni siquiera eso: README.Rmd está en .Rbuildignore).
  4. Imports.

Regla práctica: si tu paquete no funciona sin esa dependencia, va en Imports.

Desafío 6.2: Un error que ayude 10 minutos

  1. Agregá cli como dependencia con usethis::use_package("cli").
  2. Modificá dicho_al_azar() para que, si el país no está en los datos, dé un error con cli::cli_abort() que diga qué país pidió la persona y cuáles están disponibles.
  3. Cargá y probá con dicho_al_azar(pais = "Narnia").
  4. Corré devtools::check(). Si aparece un WARNING por caracteres no ASCII, arreglalo.
dicho_al_azar <- function(pais = NULL) {
  opciones <- dichos

  if (!is.null(pais)) {
    if (!pais %in% dichos$pais) {
      cli::cli_abort(c(
        "No tengo dichos de {.val {pais}}.",
        "i" = "Los pa\u00edses disponibles son: {.val {unique(dichos$pais)}}."
      ))
    }
    opciones <- dichos[dichos$pais == pais, ]
  }

  fila <- sample(nrow(opciones), size = 1)
  opciones$dicho[fila]
}
devtools::load_all()
dicho_al_azar(pais = "Narnia")
#> Error in `dicho_al_azar()`:
#> ! No tengo dichos de "Narnia".
#> ℹ Los países disponibles son: "Argentina", "Brasil", "Chile", ...

Si escribiste países con tilde, check() da un WARNING. La tilde se escribe \u00ed.

Desafío 6.3: ¿Qué está mal? 2 minutos

Una persona escribió esto en R/dicho_del_dia.R. ¿Qué le dirías?

library(cli)

dicho_del_dia <- function(fecha = Sys.Date()) {
  if (!inherits(fecha, "Date")) {
    cli_abort("{.arg fecha} tiene que ser una fecha.")
  }
  # ...
}

Nunca uses library() adentro del código de un paquete. En un paquete, el código de R/ se corre cuando se construye el paquete, no cuando se usa, y library() cambiaría la sesión de quien lo usa. Lo correcto: declarar la dependencia con usethis::use_package("cli") y llamar a la función con cli::cli_abort().

7. Tests

Desafío 7.1: Tus primeros tests 8 minutos

  1. Corré usethis::use_testthat(3) y usethis::use_test("dicho_al_azar").
  2. Escribí un test que compruebe que dicho_al_azar() devuelve un solo texto.
  3. Escribí un test que compruebe que, para un país que no existe, la función da un error.
  4. Corré los tests con devtools::test().
test_that("dicho_al_azar() devuelve un solo texto", {
  resultado <- dicho_al_azar()
  expect_type(resultado, "character")
  expect_length(resultado, 1)
})

test_that("dicho_al_azar() avisa si no conoce el país", {
  expect_error(dicho_al_azar(pais = "Narnia"), "Narnia")
})
devtools::test()
#> [ FAIL 0 | WARN 0 | SKIP 0 | PASS 3 ]

Desafío 7.2: Rompé el paquete a propósito 3 minutos

En R/dicho_al_azar.R, cambiá size = 1 por size = 2. Antes de correr los tests, pensá: ¿qué test va a fallar?

Corré devtools::test(), leé el mensaje y después volvé a dejar size = 1.

Falla expect_length(resultado, 1), porque ahora la función devuelve dos dichos:

── Failure: dicho_al_azar() devuelve un solo texto ──
`resultado` has length 2, not length 1.

Romper el código a propósito es una forma de comprobar que los tests sirven.

Desafío 7.3: Una función nueva con su test para quien termine antes

Escribí la función buscar_dicho(palabra), que devuelve las filas de dichos donde la palabra aparece en el dicho o en el significado. Pista: grepl(palabra, dichos$dicho, ignore.case = TRUE).

Documentala, escribí un test con usethis::use_test("buscar_dicho") y corré devtools::check().

#' Buscar dichos por palabra
#'
#' Busca una palabra en los dichos y en sus significados.
#'
#' @param palabra Texto a buscar. No distingue mayúsculas de minúsculas.
#'
#' @return Un data frame con las filas de [dichos] que contienen la palabra.
#' @export
#'
#' @examples
#' buscar_dicho("dinero")
buscar_dicho <- function(palabra) {
  encontrados <- grepl(palabra, dichos$dicho, ignore.case = TRUE) |
    grepl(palabra, dichos$significado, ignore.case = TRUE)
  dichos[encontrados, ]
}
test_that("buscar_dicho() encuentra dichos", {
  resultado <- buscar_dicho("dinero")
  expect_s3_class(resultado, "data.frame")
  expect_gt(nrow(resultado), 0)
})

test_that("buscar_dicho() no distingue mayúsculas", {
  expect_equal(buscar_dicho("PERRO"), buscar_dicho("perro"))
})

La versión completa, con un mensaje cuando no encuentra nada, está en el paquete terminado.

8. Compartir el paquete

Desafío 8.1: Un README y a instalar 10 minutos

  1. Creá el README con usethis::use_readme_rmd().
  2. Escribí una oración sobre qué hace el paquete y un ejemplo con dicho_al_azar().
  3. Construilo con devtools::build_readme().
  4. Instalá el paquete con devtools::install().
  5. Reiniciá R, o mejor, abrí otro proyecto, y escribí library(dichosos) y dicho_al_azar().

¡Si te salió un dicho, ya tenés tu primer paquete instalado! 🎉

# dichosos

¡Dichos y expresiones populares de América Latina, directo a tu consola de R!

## Ejemplo


::: {.cell}

```{.r .cell-code}
library(dichosos)
dicho_al_azar(pais = "Chile")
```
:::
devtools::build_readme()
devtools::install()

En otra sesión:

library(dichosos)
dicho_al_azar()

Desafío 8.2 (opcional, si tenés GitHub configurado): Al mundo 5 minutos

  1. usethis::use_git() y aceptá hacer el primer commit.
  2. usethis::use_github().
  3. Pegá el enlace a tu repositorio en el documento colaborativo.
  4. Instalá el paquete de otra persona del taller con pak::pak("usuario/dichosos"). ¿Qué dichos agregó?

Desafío 8.3: Para cerrar 3 minutos

En el documento colaborativo (o en tus notas adhesivas):

  • Para mantener: una cosa que aprendiste hoy y te gustó.
  • Para mejorar: una cosa que mejorarías del taller, o que todavía no te quedó clara.

Y una más: ¿qué código tuyo vas a convertir en paquete?

9. La IA en el ciclo de desarrollo

Desafío 9.1: ¿Dónde sí y dónde no? 5 minutos, en parejas

Para cada tarea de dichosos, decidí: ¿le darías el trabajo a una herramienta de IA, la usarías solo para un borrador que después revisás, o la harías vos?

  1. Elegir el nombre del paquete.
  2. Escribir el código de buscar_dicho().
  3. Escribir los significados de los dichos de tu país.
  4. Escribir los tests de dicho_del_dia().
  5. Arreglar el WARNING de caracteres no ASCII.
  6. Escribir el README.
  7. Responder un issue de otra persona que encontró un error en tu paquete.

Anotá tus respuestas en el documento colaborativo. No hay una sola respuesta correcta: lo interesante es el por qué.

  1. Vos. La IA no sabe si un nombre es una mala palabra en tu país (pkg_name_check() tampoco), y el nombre es una decisión de diseño.
  2. IA, y después vos. Pedile algo específico, probalo en la consola con load_all() y leé cada línea. Si no entendés el código, no lo agregues.
  3. Vos. Los datos son tu responsabilidad. Un modelo de lenguaje puede inventar un dicho o un significado con total seguridad, y en @source dice de dónde salieron.
  4. IA, con supervisión. A los agentes les encanta escribir tests: tu trabajo es que sean pocos, claros y que de verdad fallen cuando algo se rompe.
  5. IA. Es un problema mecánico, con un mensaje de check() claro. Las herramientas de IA son muy buenas arreglando lo que encuentra check().
  6. Borrador. La IA puede armar la estructura, pero el README es comunicación entre personas: revisalo, escribilo con tu voz y contá que usaste IA.
  7. Vos. Podés usar IA para entender el error o probar un arreglo, pero la respuesta es comunicación personal: la escribís vos. Si la usaste para traducir, está bien, y lo decís. Son pautas que pedimos en rOpenSci.

Desafío 9.2: Revisá lo que escribió la IA 7 minutos

Le pedimos a un asistente de IA: “Agregá a dichosos una función que devuelva los dichos de un tema”. Esto es lo que escribió. Con lo que aprendiste hoy, encontrá al menos cinco problemas.

library(dplyr)

# Esta función recibe un tema y devuelve los dichos de ese tema.
# Primero filtra los datos por tema y después devuelve el resultado.
dichos_por_tema <- function(tema) {
  if (!tema %in% dichos$tema) {
    stop("No hay dichos de ese tema. Probá con otro.")
  }
  dichos %>% filter(tema == tema)
}

Y en tests/testthat/test-dichos_por_tema.R:

test_that("dichos_por_tema() funciona", {
  expect_true(TRUE)
  resultado <- dichos_por_tema("dinero")
  expect_s3_class(resultado, "data.frame")
  expect_equal(nrow(resultado), 6)
  expect_equal(ncol(resultado), 4)
  expect_equal(names(resultado), c("dicho", "significado", "pais", "tema"))
  expect_true(all(resultado$tema == "dinero"))
  expect_error(dichos_por_tema("dinosaurios"))
})

En la función:

  1. library(dplyr) en el código del paquete. Nunca. Hay que declarar la dependencia con usethis::use_package("dplyr") y usar dplyr::filter(). O mejor: ¿hace falta dplyr? Con R base alcanza (dichos[dichos$tema == tema, ]) y el paquete tiene una dependencia menos.
  2. filter(tema == tema) no filtra nada. Adentro de filter(), los dos tema son la columna, así que la condición siempre es verdadera y devuelve todos los dichos. Es el tipo de error que solo aparece si probás la función en la consola.
  3. No tiene documentación de roxygen2 ni @export.
  4. “Probá” tiene tilde en el código: check() da un WARNING. Va Prob\u00e1.
  5. El error no ayuda: no dice qué tema se pidió ni cuáles existen. Mejor cli::cli_abort() con la lista de temas, como hicimos en dicho_al_azar().
  6. Los comentarios explican el qué, no el por qué. Si el código se entiende, sobran.

En el test:

  1. expect_true(TRUE) no comprueba nada.
  2. expect_equal(nrow(resultado), 6) se rompe cada vez que alguien suma un dicho de dinero. Es un test frágil.
  3. Es un solo test gigante. Mejor varios test_that() cortos, cada uno con una descripción de lo que comprueba.
  4. Lo bueno: all(resultado$tema == "dinero") sí detecta el error del punto 2… pero solo si alguien corre los tests. Revisar lo que escribe la IA incluye correr los tests y probar en la consola.

Desafío 9.3 (opcional, con GitHub Copilot Free u otro asistente): Pedilo bien para casa

Escribí un pedido corto y preciso para que un asistente agregue dichos_por_tema() a tu paquete. Con tu cuenta de GitHub podés usar el chat de Copilot en https://github.com/copilot. Por ejemplo:

Agregá a dichosos la función dichos_por_tema(tema) en R/dichos_por_tema.R. Usá R base, sin dependencias nuevas. Si el tema no existe, dá un error con cli::cli_abort() que liste los temas disponibles, como en dicho_al_azar(). Documentala con roxygen2 y exportala. Escribí tests cortos en tests/testthat/test-dichos_por_tema.R. Antes de escribir código, mostrame un plan.

Después: leé el plan, pedile el código, copialo a tu paquete, revisá los cambios con Git, probá la función con load_all() y corré devtools::check(). ¿Qué hizo distinto a lo que habrías hecho vos?