-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathBig-picture.qmd
279 lines (185 loc) · 13 KB
/
Big-picture.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# Panorama general {#sec-meta-big-picture}
```{r, include = FALSE}
source("common.R")
```
## 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 {.unnumbered}
Cada sección de este capítulo presenta una gran idea nueva:
- La @sec-code-data muestra que el código son datos y le enseña cómo crear y modificar expresiones mediante la captura de código.
- La @sec-code-tree describe la estructura del código en forma de árbol, llamada árbol de sintaxis abstracta.
- La @sec-coding-code muestra cómo crear nuevas expresiones programáticamente.
- La @sec-eval-intro) muestra cómo ejecutar expresiones evaluándolas en un entorno.
- La @sec-eval-funs ilustra cómo personalizar la evaluación proporcionando funciones personalizadas en un nuevo entorno.
- La @sec-eval-data extiende esa personalización a las máscaras de datos, que desdibujan la línea entre los entornos y los data frames.
- La @sec-quosure-intro introduce una nueva estructura de datos llamada quosure que hace que todo esto sea más simple y correcto.
### Requisitos previos {.unnumbered}
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.
```{r setup}
library(rlang)
library(lobstr)
```
Asegúrese de que también está familiarizado con las estructuras de datos del entorno (@sec-env-basics) y del data frame (@sec-tibble).
## El código es datos {#sec-code-data}
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:
```{r}
expr(mean(x, na.rm = TRUE))
expr(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 @sec-expressions.
`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:
```{r}
capture_it <- function(x) {
expr(x)
}
capture_it(a + b + c)
```
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:
```{r}
capture_it <- function(x) {
enexpr(x)
}
capture_it(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 @sec-vocabulary.
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 `$`:
```{r}
f <- expr(f(x = 1, y = 2))
# Agregar un nuevo argumento
f$z <- 3
f
# Or eliminar un argumento
f[[2]] <- NULL
f
```
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 @sec-expression-details.
## El código es un árbol {#sec-code-tree}
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"`).
```{r}
lobstr::ast(f(a, "b"))
```
Las llamadas a funciones anidadas crean árboles con ramificaciones más profundas:
```{r}
lobstr::ast(f1(f2(a, b), f3(1, f4(2))))
```
Debido a que todas las formas de función se pueden escribir en forma de prefijo (@sec-prefix-form), cada expresión R se puede mostrar de esta manera:
```{r}
lobstr::ast(1 + 2 * 3)
```
Mostrar el AST de esta manera es una herramienta útil para explorar la gramática de R, el tema de la @sec-grammar.
## El código puede generar código {#sec-coding-code}
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.
```{r}
call2("f", 1, 2, 3)
call2("+", 1, call2("*", 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:
```{r}
xx <- expr(x + x)
yy <- expr(y + y)
expr(!!xx / !!yy)
```
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:
```{r}
cv <- function(var) {
var <- enexpr(var)
expr(sd(!!var) / mean(!!var))
}
cv(x)
cv(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:
```{r}
cv(`)`)
```
Tratar con nombres raros[^big-picture-1] 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.
[^big-picture-1]: Más técnicamente, estos se denominan nombres no sintácticos y son el tema de la @sec-non-syntactic.
## Código de ejecución de evaluación {#sec-eval-intro}
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 @sec-evaluation.
La herramienta principal para evaluar expresiones es `base::eval()`, que toma una expresión y un entorno:
```{r}
eval(expr(x + y), env(x = 1, y = 10))
eval(expr(x + y), env(x = 2, y = 100))
```
Si omite el entorno, `eval` usa el entorno actual:
```{r}
x <- 10
y <- 100
eval(expr(x + y))
```
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.
## Personalización de la evaluación con funciones {#sec-eval-funs}
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 @sec-translation 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:
```{r}
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)
string_math(("x" * 2 + "-y") * 3)
```
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:
```{r, messasge = FALSE}
library(dplyr)
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()
DBI::dbDisconnect(con)
```
## Personalización de la evaluación con datos {#sec-eval-data}
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 (@sec-base-evaluation), 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:
```{r}
df <- data.frame(x = 1:5, y = sample(5))
eval_tidy(expr(x + y), df)
```
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 @sec-data-masks 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()`:
```{r}
with2 <- function(df, expr) {
eval_tidy(enexpr(expr), df)
}
with2(df, x + y)
```
Desafortunadamente, esta función tiene un error sutil y necesitamos una nueva estructura de datos para ayudar a solucionarlo.
## Quosures {#sec-quosure-intro}
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.
```{r}
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):
```{r}
df <- data.frame(x = 1:3)
a <- 10
with2(df, x + a)
```
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()`:
```{r}
with2 <- function(df, expr) {
a <- 1000
eval_tidy(enquo(expr), df)
}
with2(df, x + a)
```
Siempre que utilice una máscara de datos, siempre debe utilizar `enquo()` en lugar de `enexpr()`. Este es el tema del @sec-evaluation.