11  Dependencias: en la práctica

Este capítulo presenta los detalles prácticos de cómo trabajar con sus dependencias dentro de su paquete. Si necesita un repaso sobre cualquiera de los antecedentes:

Finalmente estamos listos para hablar sobre cómo usar diferentes tipos de dependencias dentro de las diferentes partes de su paquete:

11.1 Confusión sobre Imports

Dejemos esto muy claro:

Incluir un paquete en Imports en DESCRIPTION no “importa” ese paquete.

Es natural suponer que enumerar un paquete en Imports en realidad “importa” el paquete, pero esto es sólo una elección desafortunada de nombre para el campo Imports. El campo Imports garantiza que los paquetes enumerados allí estén instalados cuando se instala su paquete. No pone esas funciones a su disposición, por ejemplo debajo de R/, ni a su usuario.

No es automático ni necesariamente aconsejable que un paquete listado en Imports también aparezca en NAMESPACE a través de imports() o importFrom(). Es común que un paquete aparezca en Imports en DESCRIPTION, pero no en NAMESPACE. Lo contrario no es cierto. Cada paquete mencionado en NAMESPACE también debe estar presente en los campos Imports o Depends.

11.2 Convenciones para este capítulo

A veces nuestros ejemplos pueden incluir funciones reales de paquetes reales. Pero si necesitamos hablar de un paquete o función genérica, estas son las convenciones que usamos a continuación:

  • pkg: el nombre de su paquete hipotético

  • aaapkg o bbbpkg: el nombre de un paquete hipotético del que depende su paquete

  • aaa_fun(): el nombre de una función exportada por aaapkg

11.3 Flujo de trabajo de NAMESPACE

En las secciones siguientes, brindamos instrucciones prácticas sobre cómo (y cuándo) importar funciones de otro paquete al suyo y cómo exportar funciones desde su paquete. El archivo que realiza un seguimiento de todo esto es el archivo NAMESPACE (más detalles en Sección 10.2.2).

En el flujo de trabajo de devtools y en este libro, generamos el archivo NAMESPACE a partir de comentarios especiales en los archivos R/*.R. Dado que el paquete que finalmente hace este trabajo es roxygen2, estos se denominan “comentarios de roxygen”. Estos comentarios de roxygen también son la base de los temas de ayuda de su paquete, que se tratan en Sección 16.1.1.

El archivo NAMESPACE comienza con una única línea comentada que explica la situación (y, con suerte, desaconseja cualquier edición manual):

# Generated by roxygen2: do not edit by hand

A medida que incorpora etiquetas roxygen para exportar e importar funciones, necesita volver a generar el archivo NAMESPACE periódicamente. Este es el flujo de trabajo general para regenerar NAMESPACE (y su documentación):

  1. Agregue etiquetas relacionadas con el espacio de nombres a los comentarios de roxygen en sus archivos R/*.R. Este es un ejemplo artificial, pero te da la idea básica:

    #' @importFrom aaapkg aaa_fun
    #' @import bbbpkg
    #' @export
    foo <- function(x, y, z) {
      ...
    }
  2. Ejecute devtools::document() (o presione Ctrl/Cmd + Shift + D en RStudio) para “documentar” su paquete. Por defecto, suceden dos cosas:

    • Los temas de ayuda en los archivos man/*.Rd están actualizados (cubiertos en Capítulo 16).

    • Se vuelve a generar el archivo NAMESPACE. En nuestro ejemplo, el archivo NAMESPACE se vería así:

      # Generated by roxygen2: do not edit by hand
      
      export(foo)
      import(bbbpkg)
      importFrom(aaapkg,aaa_fun)

Roxygen2 es bastante inteligente e insertará la directiva apropiada en NAMESPACE, es decir, generalmente puede determinar si usar export() o S3method().

RStudio

Presione Ctrl/Cmd + Shift + D para generar el NAMESPACE de su paquete (y los archivos man/*.Rd). Esto también está disponible a través de Document en el menú y panel Build.

11.4 El paquete aparece en Imports

Considere una dependencia que aparece en DESCRIPTIO en Imports:

Imports:
    aaapkg

El código dentro de su paquete puede asumir que aaapkg está instalado cada vez que se instala pkg.

11.4.1 En el código debajo de R/

Nuestro valor predeterminado recomendado es llamar a funciones externas usando la sintaxis paquete::función():

somefunction <- function(...) {
  ...
  x <- aaapkg::aaa_fun(...)
  ...
}

Específicamente, le recomendamos que de forma predeterminada no importe nada desde aaapkg a su espacio de nombres. Esto hace que sea muy fácil identificar qué funciones se encuentran fuera de su paquete, lo cual es especialmente útil cuando lea su código en el futuro. Esto también elimina cualquier preocupación sobre conflictos de nombres entre aaapkg y su paquete.

Por supuesto, hay razones para hacer excepciones a esta regla e importar algo de otro paquete al suyo:

  • Un operador: No puedes llamar a un operador desde otro paquete vía ::, por lo que debes importarlo. Ejemplos: el operador de fusión nula %||% de rlang o la canalización original %>% de magrittr.

  • Una función que usas mucho: si importar una función hace que tu código sea mucho más legible, esa es una buena razón para importarla. Esto literalmente reduce la cantidad de caracteres necesarios para llamar a la función externa. Esto puede ser especialmente útil al generar mensajes dirigidos al usuario, porque hace que sea más probable que las líneas del código fuente correspondan a las líneas de la salida.

  • Una función que se llama en un bucle cerrado: hay una pequeña penalización de rendimiento asociada con ::. Es del orden de 100 ns, por lo que solo importará si llamas a la función millones de veces.

Una función útil para su flujo de trabajo interactivo es usethis::use_import_from():

usethis::use_import_from("glue", "glue_collapse")

La llamada anterior escribe esta etiqueta roxygen en el código fuente de su paquete:

#' @importFrom glue glue_collapse

¿Dónde debería ir esta etiqueta de roxygen? Hay dos ubicaciones razonables:

  • Lo más parecido posible al uso de la función externa. Con esta mentalidad, colocaría @importFrom en el comentario de roxygen encima de la función en su paquete donde usa la función externa. Si este es tu estilo, tendrás que hacerlo a mano. Descubrimos que esto parece natural al principio, pero comienza a fallar a medida que se utilizan más funciones externas en más lugares.

  • En una ubicación central. Este enfoque mantiene todas las etiquetas @importFrom juntas, en una sección dedicada del archivo de documentación a nivel de paquete (que se puede crear con usethis::use_package_doc(), Sección 16.7). Esto es lo que implementa use_import_from(). Entonces, en R/pkg-package.R, terminarás con algo como esto:

    # El siguiente bloque es utilizado por usethis para administrar automáticamente
    # etiquetas de espacio de nombres de roxygen. ¡Modifique con cuidado!
    ## usethis namespace: start
    #' @importFrom glue glue_collapse
    ## usethis namespace: end
    NULL

Recuerde que devtools::document() procesa sus comentarios de roxygen (Sección 11.3), que escribe temas de ayuda en man/*.Rd y, relevante para nuestro objetivo actual, genera el NAMESPACE archivo. Si usa use_import_from(), lo hace por usted y también llama a load_all(), haciendo que la función recién importada esté disponible en su sesión actual.

La etiqueta roxygen anterior hace que esta directiva aparezca en el archivo NAMESPACE:

importFrom(glue, glue_collapse)

Ahora puedes usar la función importada directamente en tu código:

somefunction <- function(...) {
  ...
  x <- glue_collapse(...)
  ...
}

A veces haces un uso tan intenso de tantas funciones de otro paquete que deseas importar su espacio de nombres completo. Esto debería ser relativamente raro. En tidyverse, el paquete que más comúnmente tratamos de esta manera es rlang, que funciona casi como un paquete base para nosotros.

Aquí está la etiqueta roxygen que importa todo rlang. Esto debería aparecer en algún lugar de R/*.R, como el espacio dedicado descrito anteriormente para recopilar todas las etiquetas de importación de espacios de nombres.

#' @import rlang

Después de llamar a devtools::document(), esta etiqueta roxygen hace que esta directiva aparezca en el archivo NAMESPACE:

import(rlang)

Esta es la solución menos recomendada porque puede hacer que su código sea más difícil de leer (no puede saber de dónde proviene una función) y si @importa muchos paquetes, aumenta la posibilidad de que se produzcan conflictos con los nombres de las funciones. Guárdelo para situaciones muy especiales.

11.4.1.1 Cómo no usar un paquete en Importaciones

A veces tienes un paquete listado en Imports, pero en realidad no lo usas dentro de tu paquete o, al menos, R no cree que lo uses. Eso lleva a una NOTE de R CMD check:

* checking dependencies in R code ... NOTE
Namespace in Imports field not imported from: ‘aaapkg’
  All declared Imports should be used.

Esto puede suceder si necesita enumerar una dependencia indirecta en “Importaciones”, tal vez para indicar una versión mínima para ella. El metapaquete tidyverse tiene este problema a gran escala, ya que existe principalmente para instalar un conjunto de paquetes en versiones específicas. Otro escenario es cuando su paquete usa una dependencia de tal manera que requiere otro paquete que solo es sugerido por la dependencia directa1. Hay varias situaciones en las que no es obvio que su paquete realmente necesite todos los paquetes enumerados en “Importaciones”, pero de hecho es así.

¿Cómo puede deshacerse de esta “NOTA”?

Nuestra recomendación es colocar una referencia calificada de espacio de nombres (no una llamada) a un objeto en aaapkg en algún archivo debajo de R/, como un archivo .R asociado con la configuración de todo el paquete:

ignore_unused_imports <- function() {
  aaapkg::aaa_fun
}

No es necesario llamar a ignore_unused_imports() en ningún lado. No deberías exportarlo. En realidad, no es necesario ejercitar aaapkg::aaa_fun(). Lo importante es acceder a algo en el espacio de nombres de aaapkg con ::.

Un método alternativo que podría verse tentado a utilizar es importar aaapkg::aaa_fun() al espacio de nombres de su paquete, probablemente con la etiqueta roxygen @importFrom aaapkg aaa_fun. Esto suprime la “NOTA”, pero también hace más. Esto hace que aaapkg se cargue cada vez que se carga su paquete. Por el contrario, si utiliza el método que recomendamos, el aaapkg sólo se cargará si su usuario hace algo que realmente lo requiera. Esto rara vez importa en la práctica, pero siempre es bueno minimizar o retrasar la carga de paquetes adicionales.

11.4.2 En el código de prueba

Consulte las funciones externas en sus pruebas tal como las hace en el código debajo de R/. Generalmente esto significa que debes usar aaapkg::aaa_fun(). Pero si ha importado una función particular, ya sea específicamente o como parte de un espacio de nombres completo, puede llamarla directamente en su código de prueba.

Generalmente es una mala idea usar library(aaapkg) para adjuntar una de sus dependencias en algún lugar de sus pruebas, porque hace que la ruta de búsqueda en sus pruebas sea diferente de cómo funciona realmente su paquete. Esto se trata con más detalle en Sección 14.2.5.

11.4.3 En ejemplos y viñetas

Si usa un paquete que aparece en Imports en uno de sus ejemplos o viñetas, deberá adjuntar el paquete con library(aaapkg) o usar una llamada de estilo aaapkg::aaa_fun(). Puedes asumir que aaapkg está disponible, porque eso es lo que garantiza Imports. Lea más en Sección 16.5.4 y Sección 17.4.

11.5 El paquete aparece en Suggests

Considere una dependencia que aparece en DESCRIPTION en Suggests:

Suggests:
    aaapkg

NO puede asumir que todos los usuarios han instalado aaapkg (pero puede asumir que un desarrollador sí lo ha hecho). Que un usuario tenga aaapkg dependerá de cómo instaló su paquete. La mayoría de las funciones que se utilizan para instalar paquetes admiten un argumento de “dependencias” que controla si se instalan solo las dependencias físicas o se adopta un enfoque más amplio, que incluye paquetes sugeridos:

install.packages(dependencies =)
remotes::install_github(dependencies =)
pak::pkg_install(dependencies =)

En términos generales, lo predeterminado es no instalar paquetes en Suggests.

11.5.1 En el código debajo de R/

Dentro de una función en su propio paquete, verifique la disponibilidad de un paquete sugerido con requireNamespace("aaapkg", quietly = TRUE). Hay dos escenarios básicos: la dependencia es absolutamente necesaria o su paquete ofrece algún tipo de comportamiento alternativo.

# el paquete sugerido es obligatorio 
my_fun <- function(a, b) {
  if (!requireNamespace("aaapkg", quietly = TRUE)) {
    stop(
      "Package \"aaapkg\" must be installed to use this function.",
      call. = FALSE
    )
  }
  # código que incluye llamadas como aaapkg::aaa_fun()
}

# el paquete sugerido es opcional; hay un método alternativo disponible
my_fun <- function(a, b) {
  if (requireNamespace("aaapkg", quietly = TRUE)) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}

El paquete rlang tiene algunas funciones útiles para comprobar la disponibilidad del paquete: rlang::check_installed() y rlang::is_installed(). Así es como podrían verse las comprobaciones de un paquete sugerido si usas rlang:

# el paquete sugerido es obligatorio
my_fun <- function(a, b) {
  rlang::check_installed("aaapkg", reason = "to use `aaa_fun()`")
  # código que incluye llamadas como aaapkg::aaa_fun()
}

# el paquete sugerido es opcional; hay un método alternativo disponible
my_fun <- function(a, b) {
  if (rlang::is_installed("aaapkg")) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}

Estas funciones de rlang tienen características útiles para la programación, como vectorización sobre pkg, errores clasificados con una carga útil de datos y, para check_installed(), una oferta para instalar el paquete necesario en una sesión interactiva.

11.5.2 En el código de prueba

El equipo de tidyverse generalmente escribe pruebas como si todos los paquetes sugeridos estuvieran disponibles. Es decir, los utilizamos incondicionalmente en las pruebas.

La motivación para esta postura es la autoconsistencia y el pragmatismo. El paquete clave necesario para ejecutar pruebas es testthat y aparece en Suggests, no en Imports o Depends. Por lo tanto, si las pruebas realmente se están ejecutando, eso implica que se ha aplicado una noción amplia de dependencias de paquetes.

Además, empíricamente, en cada escenario importante de ejecución de R CMD check, se instalan los paquetes sugeridos. Esto es generalmente cierto para CRAN y nos aseguramos de que así sea en nuestras propias comprobaciones automatizadas. Sin embargo, es importante tener en cuenta que otros mantenedores de paquetes adoptan una postura diferente y optan por proteger todo uso de los paquetes sugeridos en sus pruebas y viñetas.

A veces incluso hacemos una excepción y protegemos el uso de un paquete sugerido en una prueba. Aquí hay una prueba de ggplot2, que usa testthat::skip_if_not_installed() para omitir la ejecución si el paquete sf sugerido no está disponible.

test_that("basic plot builds without error", {
  skip_if_not_installed("sf")

  nc_tiny_coords <- matrix(
    c(-81.473, -81.741, -81.67, -81.345, -81.266, -81.24, -81.473,
      36.234, 36.392, 36.59, 36.573, 36.437, 36.365, 36.234),
    ncol = 2
  )

  nc <- sf::st_as_sf(
    data_frame(
      NAME = "ashe",
      geometry = sf::st_sfc(sf::st_polygon(list(nc_tiny_coords)), crs = 4326)
    )
  )

  expect_doppelganger("sf-polygons", ggplot(nc) + geom_sf() + coord_sf())
})

¿Qué podría justificar el uso de skip_if_not_installed()? En este caso, la instalación del paquete sf puede no ser fácil y es posible que un colaborador quiera ejecutar las pruebas restantes, incluso si sf no está disponible.

Finalmente, tenga en cuenta que testthat::skip_if_not_installed(pkg, minimal_version = "xyz") se puede usar para omitir condicionalmente una prueba según la versión del otro paquete.

11.5.3 En ejemplos y viñetas

Otro lugar común para usar un paquete sugerido es en un ejemplo y aquí a menudo lo protegemos con require() o requireNamespace(). Este ejemplo es de ggplot2::coord_map(). ggplot2 enumera el paquete de mapas en Suggests.

#' @examples
#' if (require("maps")) {
#'   nz <- map_data("nz")
#'   # Prepara un mapa de Nueva Zelanda
#'   nzmap <- ggplot(nz, aes(x = long, y = lat, group = group)) +
#'     geom_polygon(fill = "white", colour = "black")
#'  
#'   # Grafica en cordenadas cartesianas
#'   nzmap
#' }

Un ejemplo es básicamente el único lugar donde usaríamos require() dentro de un paquete. Lea más en Sección 10.4.

Nuestra postura con respecto al uso de paquetes sugeridos en viñetas es similar a la de las pruebas. Los paquetes clave necesarios para crear viñetas (rmarkdown y knitr) se enumeran en Suggests. Por lo tanto, si se están creando las viñetas, es razonable suponer que todos los paquetes sugeridos están disponibles. Normalmente utilizamos paquetes sugeridos incondicionalmente dentro de viñetas.

Pero si elige utilizar paquetes sugeridos de forma condicional en sus viñetas, la opción knitr chunk eval es muy útil para lograrlo. Consulte Sección 17.4 para obtener más información.

11.6 El paquete aparece en Depends

Considere una dependencia que aparece en DESCRIPTION en Depends:

Depends:
    aaapkg

Esta situación tiene mucho en común con un paquete listado en Imports. El código dentro de su paquete puede asumir que aaapkg está instalado en el sistema. La única diferencia es que aaapkg se adjuntará cada vez que se envíe su paquete.

11.6.1 En el código debajo de R/ y en el código de prueba

Sus opciones son exactamente las mismas que usar funciones de un paquete enumerado en Imports:

  • Utilice la sintaxis aaapkg::aaa_fun().

  • Importe una función individual con la etiqueta roxygen @importFrom aaapkg aaa_fun y llame a aaa_fun() directamente.

  • Importe todo el espacio de nombres aaapkg con la etiqueta roxygen @import aaapkg y llame a cualquier función directamente.

La principal diferencia entre esta situación y una dependencia enumerada en Imports es que es mucho más común importar el espacio de nombres completo de un paquete enumerado en Depends. Esto a menudo tiene sentido, debido a la relación de dependencia especial que motivó su inclusión en Depends en primer lugar.

11.6.2 En ejemplos y viñetas

Esta es la diferencia más obvia entre una dependencia en Depends versus Imports. Dado que su paquete se adjunta cuando se ejecutan sus ejemplos, también se adjunta el paquete que figura en Depends. No es necesario adjuntarlo explícitamente con library(aaapkg).

El paquete ggforce depende de ggplot2 y los ejemplos de ggforce::geom_mark_rect() usan funciones como ggplot2::ggplot() y ggplot2::geom_point() sin ninguna llamada explícita a library(ggplot2):

ggplot(iris, aes(Petal.Length, Petal.Width)) +
  geom_mark_rect(aes(fill = Species, filter = Species != 'versicolor')) +
  geom_point()
# el código ejemplo continua ...

La primera línea de código ejecutada en una de sus viñetas es probablemente library(pkg), que adjunta su paquete y, como efecto secundario, adjunta cualquier dependencia enumerada en Depends. No es necesario adjuntar explícitamente la dependencia antes de usarla. El paquete censored depende del paquete de suvirval y el código en vignette("examples", package = "censored") comienza así:

library(tidymodels)
library(censored)
#> Loading required package: survival

# código de viñeta continua ...

11.7 El paquete es una dependencia no estándar

En los paquetes desarrollados con devtools, es posible que vea archivos DESCRIPTION que utilizan un par de campos no estándar para dependencias de paquetes específicas para tareas de desarrollo.

11.7.1 Dependiendo de la versión de desarrollo de un paquete

El campo Remotes se puede utilizar cuando necesita instalar una dependencia desde un lugar no estándar, es decir, desde algún lugar además de CRAN o Bioconductor. Un ejemplo común de esto es cuando estás desarrollando contra una versión de desarrollo de una de tus dependencias. Durante este tiempo, querrás instalar la dependencia desde su repositorio de desarrollo, que suele ser GitHub. La forma de especificar varias fuentes remotas se describe en una viñeta de devtools y en un tema de ayuda del paquete.

La dependencia y cualquier requisito de versión mínima aún deben declararse de la forma habitual en, por ejemplo, Imports. usethis::use_dev_package() ayuda a realizar los cambios necesarios en DESCRIPTION. Si su paquete depende temporalmente de una versión de desarrollo de aaapkg, los campos DESCRIPTION afectados podrían evolucionar de esta manera:

Estable -->              Desarrollo -->               Estable de nuevo
----------------------   ---------------------------   ----------------------
Package: pkg             Package: pkg                  Package: pkg
Version: 1.0.0           Version: 1.0.0.9000           Version: 1.1.0
Imports:                 Imports:                      Imports: 
    aaapkg (>= 2.1.3)       aaapkg (>= 2.1.3.9000)       aaapkg (>= 2.2.0)
                         Remotes:   
                             jane/aaapkg 
CRAN

Es importante tener en cuenta que no debe enviar su paquete a CRAN en el estado intermedio, es decir, con un campo Remotes y con una dependencia requerida en una versión que no está disponible en CRAN o Bioconductor. Para los paquetes CRAN, esto solo puede ser un estado de desarrollo temporal, que eventualmente se resuelve cuando la dependencia se actualiza en CRAN y usted puede aumentar su versión mínima en consecuencia.

11.7.2 El campo Config/Needs/*

También puede ver paquetes desarrollados por devtools con paquetes enumerados en los campos DESCRIPTION en el formato Config/Needs/*, que describimos en Sección 9.8.

El uso de Config/Needs/* no está directamente relacionado con devtools. Es más exacto decir que está asociado con flujos de trabajo de integración continua puestos a disposición de la comunidad en https://github.com/r-lib/actions/ y expuesto a través de funciones como usethis::use_github_actions(). Un campo Config/Needs/* le indica a la acción de GitHub setup-r-dependencies acerca de paquetes adicionales que deben instalarse.

Config/Needs/website es el más común y proporciona un lugar para especificar paquetes que no son una dependencia formal, pero que deben estar presentes para construir el sitio web del paquete (Capítulo 19). El paquete readxl es un buen ejemplo. Tiene un artículo sin viñeta sobre flujos de trabajo que muestra a readxl trabajando en conjunto con otros paquetes de tidyverse, como readr y purrr. ¡Pero no tiene sentido que readxl tenga una dependencia formal de readr o purrr o (peor aún) tidyverse!

A la izquierda está lo que readxl tiene en el campo Configuración/Necesidades/sitio web de DESCRIPCIÓN para indicar que se necesita tidyverse para construir el sitio web, que también está formateado con un estilo que se encuentra en tidyverse/template Repositorio de GitHub. A la derecha está el extracto correspondiente de la configuración del flujo de trabajo que crea e implementa el sitio web.

en DESCRIPTION                  en .github/workflows/pkgdown.yaml
--------------------------      ---------------------------------
Config/Needs/website:           - uses: r-lib/actions/setup-r-dependencies@v2
    tidyverse,                    with:
    tidyverse/tidytemplate          extra-packages: pkgdown
                                    needs: website

Los sitios web de paquetes y la integración continua se analizan con más detalle en Capítulo 19 y Sección 20.2, respectivamente.

La convención Config/Needs/* es útil porque permite a un desarrollador usar DESCRIPTION como su registro definitivo de dependencias de paquetes, manteniendo al mismo tiempo una distinción clara entre las verdaderas dependencias de tiempo de ejecución y aquellas que solo son necesarias para tareas de desarrollo especializadas.

11.8 Exportaciones

Para que una función se pueda utilizar fuera de su paquete, debe exportarla. Cuando crea un nuevo paquete con usethis::create_package(), al principio no se exporta nada, ni siquiera una vez que agrega algunas funciones. Aún puedes experimentar interactivamente con load_all(), ya que carga todas las funciones, no solo las que se exportan. Pero si instala y adjunta el paquete con library(pkg) en una nueva sesión de R, notará que no hay funciones disponibles.

11.8.1 Qué exportar

Exporta funciones que quieras que utilicen otras personas. Las funciones exportadas deben estar documentadas y debe tener cuidado al cambiar su interfaz: ¡otras personas las están usando! Generalmente es mejor exportar muy poco que demasiado. Es fácil empezar a exportar algo que antes no hacía; Es difícil dejar de exportar una función porque podría romper el código existente. Siempre opte por el lado de la precaución y la simplicidad. Es más fácil darle a las personas más funcionalidades que quitarles cosas a las que están acostumbradas.

Creemos que los paquetes que tienen una amplia audiencia deben esforzarse por hacer una cosa y hacerlo bien. Todas las funciones de un paquete deben estar relacionadas con un único problema (o un conjunto de problemas estrechamente relacionados). Cualquier función que no esté relacionada con ese propósito no debe exportarse. Por ejemplo, la mayoría de nuestros paquetes tienen un archivo utils.R (Sección 6.1) que contiene pequeñas funciones de utilidad que son útiles internamente, pero que no forman parte del propósito principal de esos paquetes. No exportamos tales funciones. Hay al menos dos razones para esto:

  • Libertad para ser menos robusto y menos general. Una utilidad para uso interno no tiene por qué implementarse de la misma manera que una función utilizada por otros. Sólo necesita cubrir su propio caso de uso.

  • Dependencias inversas lamentables. No desea que las personas dependan de su paquete para obtener funcionalidades y funciones que no están relacionadas con su propósito principal.

Dicho esto, si estás creando un paquete para ti mismo, es mucho menos importante ser tan disciplinado. Como sabe lo que hay en su paquete, está bien tener un paquete “misceláneo” local que contenga una mezcolanza de funciones que le resulten útiles. Pero probablemente no sea una buena idea lanzar un paquete de este tipo para un uso más amplio.

A veces su paquete tiene una función que podría ser de interés para otros desarrolladores que amplíen su paquete, pero no para los usuarios típicos. En este caso, se desea exportar la función, pero también darle un perfil muy bajo en términos de documentación pública. Esto se puede lograr combinando las etiquetas roxygen @export y @keywords internal. La palabra clave internal evita que la función aparezca en el índice del paquete, pero el tema de ayuda asociado todavía existe y la función sigue apareciendo entre las exportadas en el archivo NAMESPACE.

11.8.2 Reexportación

A veces desea que algo esté disponible para los usuarios de su paquete y que en realidad lo proporciona una de sus dependencias. Cuando devtools se dividió en varios paquetes más pequeños (Sección 2.2), muchas de las funciones orientadas al usuario se trasladaron a otra parte. Para usar esto, la solución elegida fue incluirlo en Depends (Sección 10.4.1), pero esa no es una buena solución general. En cambio, devtools ahora reexporta ciertas funciones que en realidad se encuentran en un paquete diferente.

Aquí hay un modelo para reexportar un objeto de otro paquete, usando la función session_info() como nuestro ejemplo:

  1. Enumere el paquete que aloja el objeto reexportado en Imports en DESCRIPCIÓN.2 En este caso, la función session_info() se exporta mediante el paquete sessioninfo.

    Imports:
        sessioninfo
  2. En uno de sus archivos R/*.R, tenga una referencia a la función de destino, precedida por etiquetas roxygen tanto para importar como para exportar.

    #' @export
    #' @importFrom sessioninfo session_info
    sessioninfo::session_info

¡Eso es todo! La próxima vez que vuelva a generar NAMESPACE, estas dos líneas estarán allí (normalmente intercaladas con otras exportaciones e importaciones):

...
export(session_info)
...
importFrom(sessioninfo,session_info)
...

Y esto explica cómo library(devtools) hace que session_info() esté disponible en la sesión actual. Esto también conducirá a la creación del archivo man/reexports.Rd, que perfecciona el requisito de que su paquete debe documentar todas sus funciones exportadas. Este tema de ayuda enumera todos los objetos reexportados y enlaces a su documentación principal.

11.9 Importaciones y exportaciones relacionadas con S3

R tiene múltiples sistemas de programación orientada a objetos (OOP):

  • S3 es actualmente el más importante para nosotros y es lo que se aborda en este libro. El capítulo S3 de Advanced R es un buen lugar para aprender más sobre S3 conceptualmente y el paquete vctrs vale la pena estudiarlo para obtener conocimientos prácticos.

  • S4 es muy importante dentro de ciertas comunidades R, sobre todo dentro del proyecto Bioconductor. Solo usamos S4 cuando es necesario por compatibilidad con otros paquetes. Si desea obtener más información, el capítulo S4 de Advanced R es un buen punto de partida y tiene recomendaciones de recursos adicionales.

  • R6 se utiliza en muchos paquetes de tidyverse (definido en sentido amplio), pero está fuera del alcance de este libro. Algunos buenos lugares para obtener más información incluyen el sitio web del paquete R6, el capítulo R6 de Advanced R y la documentación de roxygen2 relacionada con R6.

En términos de problemas de espacio de nombres en torno a las clases de S3, lo principal a considerar son las funciones genéricas y sus implementaciones específicas de clase conocidas como métodos. Si su paquete “posee” una clase S3, tiene sentido exportar una función constructora fácil de usar. A menudo, esto es sólo una función normal y no existe un ángulo S3 especial.

Si su paquete “posee” un genérico S3 y desea que otros puedan usarlo, debe exportar el genérico. Por ejemplo, el paquete dplyr exporta la función genérica dplyr::count() y también implementa y exporta un método específico, count.data.frame():

#' ... toda la documentación habitual para count()...
#' @export
count <- function(x, ..., wt = NULL, sort = FALSE, name = NULL) {
  UseMethod("count")
}

#' @export
count.data.frame <- function(
  x,
  ...,
  wt = NULL,
  sort = FALSE,
  name = NULL,
  .drop = group_by_drop_default(x)) { ... }

Las líneas correspondientes en el archivo NAMESPACE de dplyr se ven así:

...
S3method(count,data.frame)
...
export(count)
...

Ahora imagina que tu paquete implementa un método para count() para una clase de tu “propiedad” (no data.frame). Un buen ejemplo es el paquete dbplyr, que implementa count() para la clase tbl_lazy. Un paquete complementario que implemente un genérico S3 para una nueva clase debe enumerar el paquete proveedor de genéricos en Imports, importar el genérico a su espacio de nombres y exportar su método S3. Aquí hay parte del archivo DESCRIPTION de dbplyr:

Imports: 
    ...,
    dplyr,
    ...

En dbplyr/R/verb-count.R, tenemos:

#' @importFrom dplyr count
#' @export
count.tbl_lazy <- function(x, ..., wt = NULL, sort = FALSE, name = NULL) { ... }

En NAMESPACE, tenemos:

S3method(count,tbl_lazy)
...
importFrom(dplyr,count)

Dbplyr también proporciona métodos para varios genéricos proporcionados por el paquete base, como dim() y names(). En este caso, no es necesario importar esos genéricos, pero sí es necesario exportar los métodos. En dbplyr/R/tbl_lazy.R, tenemos:

#' @export
dim.tbl_lazy <- function(x) {
  c(NA, length(op_vars(x$lazy_query)))
}

#' @export
names.tbl_lazy <- function(x) {
  colnames(x)
}

En NAMESPACE, esto produce:

S3method(dim,tbl_lazy)
...
S3method(names,tbl_lazy)

El último caso y el más complicado es cuando su paquete ofrece un método genérico “propiedad” de un paquete que ha enumerado en Suggests. La idea básica es que desea registrar la disponibilidad de su método S3 de forma condicional, cuando se carga su paquete. Si el paquete sugerido está presente, su método S3 debería estar registrado, pero en caso contrario no debería estar registrado.

Ilustraremos esto con un ejemplo. Dentro de tidyverse, el paquete glue se administra como un paquete de bajo nivel que debe tener dependencias mínimas (Sección 10.1.3). Las funciones de glue generalmente devuelven un vector de caracteres que también tiene la clase S3 "glue".

library(glue)
name <- "Betty"
(ret <- glue('My name is {name}.'))
#> My name is Betty.
class(ret)
#> [1] "glue"      "character"

La motivación para esto es que permite que glue ofrezca métodos especiales para print(), el operador + y subconjuntos mediante [ y [[. Sin embargo, una desventaja es que este atributo de clase complica las comparaciones de cadenas:

identical(ret, "My name is Betty.")
#> [1] FALSE
all.equal(ret, "My name is Betty.")
#> [1] "Attributes: < Modes: list, NULL >"                   
#> [2] "Attributes: < Lengths: 1, 0 >"                       
#> [3] "Attributes: < names for target but not for current >"
#> [4] "Attributes: < current is not list-like >"            
#> [5] "target is glue, current is character"

Por lo tanto, para las pruebas, es útil si glue ofrece un método para testthat::compare(), que explica por qué esta expectativa tiene éxito:

testthat::expect_equal(ret, "My name is Betty.")

¡Pero glue no puede incluir la prueba en Imports! Debe ir en Suggests. La solución es registrar el método de forma condicional cuando se carga glue. Aquí hay una versión redactada de la función .onLoad() de glue, donde verá que también registra condicionalmente algunos otros métodos:

.onLoad <- function(...) {
  s3_register("testthat::compare", "glue")
  s3_register("waldo::compare_proxy", "glue")
  s3_register("vctrs::vec_ptype2", "glue.glue")
  ...
  invisible()
}

La función s3_register() proviene del paquete vctrs. Si no tiene una necesidad orgánica de depender de vctrs, es común (y recomendable) simplemente insertar la fuente s3_register() en su propio paquete. No siempre puedes copiar el código de los paquetes de otras personas y pegarlo en el tuyo, pero en este caso puedes hacerlo. Este uso está específicamente permitido por la licencia del código fuente de s3_register(). Esto proporciona una gran transición hacia Capítulo 12, que tiene que ver con las licencias.


  1. Por ejemplo, si su paquete necesita llamar a ggplot2::geom_hex(), puede optar por incluir hexbin en Imports, ya que ggplot2 solo lo incluye en Suggests.↩︎

  2. Recuerde que usethis::use_package() es útil para agregar dependencias a DESCRIPTION.↩︎