17  Panorama general

17.1 Introducción

La metaprogramación es el tema más difícil de este libro porque reúne muchos temas que antes no estaban relacionados y lo obliga a lidiar con problemas en los que probablemente no había pensado antes. También necesitarás aprender mucho vocabulario nuevo, y al principio parecerá que cada término nuevo está definido por otros tres términos de los que no has oído hablar. Incluso si es un programador experimentado en otro idioma, es poco probable que sus habilidades existentes sean de mucha ayuda, ya que pocos lenguajes populares modernos exponen el nivel de metaprogramación que proporciona R. Así que no se sorprenda si se siente frustrado o confundido al principio; ¡Esta es una parte natural del proceso que le sucede a todos!

Pero creo que ahora es más fácil aprender metaprogramación que nunca. En los últimos años, la teoría y la práctica han madurado sustancialmente, brindando una base sólida junto con herramientas que le permiten resolver problemas comunes. En este capítulo, obtendrá una visión general de todas las piezas principales y cómo encajan entre sí.

Estructura

Cada sección de este capítulo presenta una gran idea nueva:

  • La Sección 17.2 muestra que el código son datos y le enseña cómo crear y modificar expresiones mediante la captura de código.

  • La Sección 17.3 describe la estructura del código en forma de árbol, llamada árbol de sintaxis abstracta.

  • La Sección 17.4 muestra cómo crear nuevas expresiones programáticamente.

  • La Sección 17.5) muestra cómo ejecutar expresiones evaluándolas en un entorno.

  • La Sección 17.6 ilustra cómo personalizar la evaluación proporcionando funciones personalizadas en un nuevo entorno.

  • La Sección 17.7 extiende esa personalización a las máscaras de datos, que desdibujan la línea entre los entornos y los data frames.

  • La Sección 17.8 introduce una nueva estructura de datos llamada quosure que hace que todo esto sea más simple y correcto.

Requisitos previos

Este capítulo presenta las grandes ideas usando rlang; aprenderá los equivalentes básicos en capítulos posteriores. También usaremos el paquete lobstr para explorar la estructura de árbol del código.

library(rlang)
library(lobstr)

Asegúrese de que también está familiarizado con las estructuras de datos del entorno (Sección 7.2) y del data frame (Sección 3.6).

17.2 El código es datos

La primera gran idea es que el código es información: puede capturar código y calcularlo como puede hacerlo con cualquier otro tipo de información. La primera forma de capturar código es con rlang::expr(). Puedes pensar en expr() como si devolviera exactamente lo que pasas:

expr(mean(x, na.rm = TRUE))
#> mean(x, na.rm = TRUE)
expr(10 + 100 + 1000)
#> 10 + 100 + 1000

Más formalmente, el código capturado se llama expresión. Una expresión no es un único tipo de objeto, sino un término colectivo para cualquiera de los cuatro tipos (llamada, símbolo, constante o lista de pares), sobre los que aprenderá más en el Capítulo 18.

expr() le permite capturar el código que ha escrito. Necesita una herramienta diferente para capturar el código pasado a una función porque expr() no funciona:

capture_it <- function(x) {
  expr(x)
}
capture_it(a + b + c)
#> x

Aquí debe usar una función diseñada específicamente para capturar la entrada del usuario en un argumento de función: enexpr(). Piensa en “en” en el contexto de “enriquecer”: enexpr() toma un argumento mal evaluado y lo convierte en una expresión:

capture_it <- function(x) {
  enexpr(x)
}
capture_it(a + b + c)
#> a + b + c

Como capture_it() usa enexpr(), decimos que cita automáticamente su primer argumento. Aprenderá más sobre este término en la Sección 19.2.1.

Una vez que haya capturado una expresión, puede inspeccionarla y modificarla. Las expresiones complejas se comportan como listas. Eso significa que puedes modificarlos usando [[ y $:

f <- expr(f(x = 1, y = 2))

# Agregar un nuevo argumento
f$z <- 3
f
#> f(x = 1, y = 2, z = 3)

# Or eliminar un argumento
f[[2]] <- NULL
f
#> f(y = 2, z = 3)

El primer elemento de la llamada es la función a llamar, lo que significa que el primer argumento está en la segunda posición. Conocerá los detalles completos en la Sección 18.3.

17.3 El código es un árbol

Para realizar manipulaciones más complejas con expresiones, debe comprender completamente su estructura. Detrás de escena, casi todos los lenguajes de programación representan el código como un árbol, a menudo llamado árbol de sintaxis abstracta, o AST para abreviar. R es inusual en el sentido de que realmente puede inspeccionar y manipular este árbol.

Una herramienta muy conveniente para comprender la estructura en forma de árbol es lobstr::ast(). Dado algo de código, esta función muestra la estructura de árbol subyacente. Las llamadas a funciones forman las ramas del árbol y se muestran mediante rectángulos. Las hojas del árbol son símbolos (como a) y constantes (como "b").

lobstr::ast(f(a, "b"))
#> █─f 
#> ├─a 
#> └─"b"

Las llamadas a funciones anidadas crean árboles con ramificaciones más profundas:

lobstr::ast(f1(f2(a, b), f3(1, f4(2))))
#> █─f1 
#> ├─█─f2 
#> │ ├─a 
#> │ └─b 
#> └─█─f3 
#>   ├─1 
#>   └─█─f4 
#>     └─2

Debido a que todas las formas de función se pueden escribir en forma de prefijo (Sección 6.8.2), cada expresión R se puede mostrar de esta manera:

lobstr::ast(1 + 2 * 3)
#> █─`+` 
#> ├─1 
#> └─█─`*` 
#>   ├─2 
#>   └─3

Mostrar el AST de esta manera es una herramienta útil para explorar la gramática de R, el tema de la Sección 18.4.

17.4 El código puede generar código

Además de ver el árbol a partir del código escrito por un humano, también puede usar el código para crear nuevos árboles. Hay dos herramientas principales: call2() y eliminación de comillas.

rlang::call2() construye una llamada de función a partir de sus componentes: la función a llamar y los argumentos para llamarla.

call2("f", 1, 2, 3)
#> f(1, 2, 3)
call2("+", 1, call2("*", 2, 3))
#> 1 + 2 * 3

call2() a menudo es conveniente para programar, pero es un poco torpe para el uso interactivo. Una técnica alternativa es construir árboles de código complejos combinando árboles de código más simples con una plantilla. expr() y enexpr() tienen soporte incorporado para esta idea a través de !! (pronunciado bang-bang), el operador sin comillas.

Los detalles precisos son el tema de la Sección @ref(unquoting), pero básicamente !!x inserta el árbol de código almacenado en x en la expresión. Esto facilita la construcción de árboles complejos a partir de fragmentos simples:

xx <- expr(x + x)
yy <- expr(y + y)

expr(!!xx / !!yy)
#> (x + x)/(y + y)

Tenga en cuenta que la salida conserva la precedencia del operador, por lo que obtenemos (x + x) / (y + y) y no x + x / y + y (es decir, x + (x / y) + y). Esto es importante, especialmente si te has estado preguntando si no sería más fácil simplemente pegar cadenas.

Quitar las comillas se vuelve aún más útil cuando lo envuelves en una función, primero usando enexpr() para capturar la expresión del usuario, luego expr() y !! para crear una nueva expresión usando una plantilla. El siguiente ejemplo muestra cómo puede generar una expresión que calcule el coeficiente de variación:

cv <- function(var) {
  var <- enexpr(var)
  expr(sd(!!var) / mean(!!var))
}

cv(x)
#> sd(x)/mean(x)
cv(x + y)
#> sd(x + y)/mean(x + y)

(Esto no es muy útil aquí, pero poder crear este tipo de bloque de construcción es muy útil cuando se resuelven problemas más complejos.)

Es importante destacar que esto funciona incluso cuando se le dan nombres de variables extraños:

cv(`)`)
#> sd(`)`)/mean(`)`)

Tratar con nombres raros1 es otra buena razón para evitar paste() al generar código R. Puede pensar que se trata de una preocupación esotérica, pero no preocuparse por ello cuando la generación de código SQL en aplicaciones web condujo a ataques de inyección de SQL que, en conjunto, han costado miles de millones de dólares.

17.5 Código de ejecución de evaluación

Inspeccionar y modificar el código le brinda un conjunto de herramientas poderosas. Obtiene otro conjunto de herramientas poderosas cuando evalúa, es decir, ejecuta o ejecuta, una expresión. Evaluar una expresión requiere un entorno, que le dice a R qué significan los símbolos en la expresión. Aprenderá los detalles de la evaluación en el Capítulo 20.

La herramienta principal para evaluar expresiones es base::eval(), que toma una expresión y un entorno:

eval(expr(x + y), env(x = 1, y = 10))
#> [1] 11
eval(expr(x + y), env(x = 2, y = 100))
#> [1] 102

Si omite el entorno, eval usa el entorno actual:

x <- 10
y <- 100
eval(expr(x + y))
#> [1] 110

Una de las grandes ventajas de evaluar el código manualmente es que puede modificar el entorno. Hay dos razones principales para hacer esto:

  • Para anular temporalmente las funciones para implementar un lenguaje específico de dominio.
  • Para agregar una máscara de datos para que pueda hacer referencia a las variables en un data frame como si fueran variables en un entorno.

17.6 Personalización de la evaluación con funciones

El ejemplo anterior usó un entorno que vinculaba x e y a vectores. Es menos obvio que también vincula nombres a funciones, lo que le permite anular el comportamiento de las funciones existentes. Esta es una gran idea a la que volveremos en el Capítulo 21 donde exploro la generación de HTML y LaTeX desde R. El siguiente ejemplo le da una idea del poder. Aquí evalúo el código en un entorno especial donde * y + han sido anulados para trabajar con cadenas en lugar de números:

string_math <- function(x) {
  e <- env(
    caller_env(),
    `+` = function(x, y) paste0(x, y),
    `*` = function(x, y) strrep(x, y)
  )

  eval(enexpr(x), e)
}

name <- "Hadley"
string_math("Hello " + name)
#> [1] "Hello Hadley"
string_math(("x" * 2 + "-y") * 3)
#> [1] "xx-yxx-yxx-y"

dplyr lleva esta idea al extremo, ejecutando código en un entorno que genera SQL para su ejecución en una base de datos remota:

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
mtcars_db <- copy_to(con, mtcars)

mtcars_db %>%
  filter(cyl > 2) %>%
  select(mpg:hp) %>%
  head(10) %>%
  show_query()
#> <SQL>
#> SELECT `mpg`, `cyl`, `disp`, `hp`
#> FROM `mtcars`
#> WHERE (`cyl` > 2.0)
#> LIMIT 10

DBI::dbDisconnect(con)

17.7 Personalización de la evaluación con datos

Reenlazar funciones es una técnica extremadamente poderosa, pero tiende a requerir una gran inversión. Una aplicación práctica más inmediata es modificar la evaluación para buscar variables en un data frame en lugar de un entorno. Esta idea impulsa las funciones base subset() y transform(), así como muchas funciones tidyverse como ggplot2::aes() y dplyr::mutate(). Es posible usar eval() para esto, pero hay algunas trampas potenciales (Sección 20.6), así que cambiaremos a rlang::eval_tidy() en su lugar.

Además de la expresión y el entorno, eval_tidy() también toma una máscara de datos, que suele ser un data frame:

df <- data.frame(x = 1:5, y = sample(5))
eval_tidy(expr(x + y), df)
#> [1] 6 6 4 6 8

Evaluar con una máscara de datos es una técnica útil para el análisis interactivo porque le permite escribir x + y en lugar de df$x + df$y. Sin embargo, esa conveniencia tiene un costo: la ambigüedad. En la Sección 20.4 aprenderá cómo lidiar con la ambigüedad usando los pronombres especiales .data y .env.

Podemos envolver este patrón en una función usando enexpr(). Esto nos da una función muy similar a base::with():

with2 <- function(df, expr) {
  eval_tidy(enexpr(expr), df)
}

with2(df, x + y)
#> [1] 6 6 4 6 8

Desafortunadamente, esta función tiene un error sutil y necesitamos una nueva estructura de datos para ayudar a solucionarlo.

17.8 Quosures

Para hacer el problema más obvio, voy a modificar with2(). El problema básico aún ocurre sin esta modificación, pero es mucho más difícil de ver.

with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enexpr(expr), df)
}

Podemos ver el problema cuando usamos with2() para referirnos a una variable llamada a. Queremos que el valor de a provenga del enlace que podemos ver (10), no del enlace interno de la función (1000):

df <- data.frame(x = 1:3)
a <- 10
with2(df, x + a)
#> [1] 1001 1002 1003

El problema surge porque necesitamos evaluar la expresión capturada en el entorno donde fue escrita (donde a es 10), no el entorno dentro de with2() (donde a es 1000).

Afortunadamente podemos resolver este problema usando una nueva estructura de datos: el quosure que agrupa una expresión con un entorno. eval_tidy() sabe cómo trabajar con quosures, así que todo lo que tenemos que hacer es cambiar enexpr() por enquo():

with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enquo(expr), df)
}

with2(df, x + a)
#> [1] 11 12 13

Siempre que utilice una máscara de datos, siempre debe utilizar enquo() en lugar de enexpr(). Este es el tema del Capítulo 20.


  1. Más técnicamente, estos se denominan nombres no sintácticos y son el tema de la Sección 2.2.1.↩︎