-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathFunction-operators.qmd
334 lines (231 loc) · 15.2 KB
/
Function-operators.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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# Operadores de funciones {#sec-function-operators}
```{r, include = FALSE}
source("common.R")
```
## Introducción
\index{function operators}
En este capítulo, aprenderá acerca de los operadores de funciones. Un **operador de función** es una función que toma una (o más) funciones como entrada y devuelve una función como salida. El siguiente código muestra un operador de función simple, `chatty()`. Envuelve una función, creando una nueva función que imprime su primer argumento. Puede crear una función como esta porque le da una ventana para ver cómo funcionan las funciones, como `map_int()`.
```{r, eval = TRUE}
chatty <- function(f) {
force(f)
function(x, ...) {
message("Processing ", x)
f(x, ...)
}
}
f <- function(x) x ^ 2
s <- c(3, 2, 1)
purrr::map_dbl(s, chatty(f))
```
Los operadores de funciones están estrechamente relacionados con las fábricas de funciones; de hecho, son solo una fábrica de funciones que toma una función como entrada. Al igual que las fábricas, no hay nada que no puedas hacer sin ellas, pero a menudo te permiten eliminar la complejidad para que tu código sea más legible y reutilizable.
Los operadores de función suelen estar emparejados con funcionales. Si está utilizando un bucle for, rara vez hay una razón para usar un operador de función, ya que hará que su código sea más complejo con poca ganancia.
Si está familiarizado con Python, los decoradores son solo otro nombre para los operadores de funciones.
### Estructura {.unnumbered}
- La @sec-existing-fos le presenta dos operadores de funciones existentes extremadamente útiles y le muestra cómo usarlos para resolver problemas reales.
- La @sec-fo-case-study funciona a través de un problema susceptible de solución con operadores de función: descargar muchas páginas web.
### Requisitos previos {.unnumbered}
Los operadores de funciones son un tipo de fábrica de funciones, así que asegúrese de estar familiarizado al menos con la @sec-function-fundamentals antes de continuar.
Usaremos [purrr](https://purrr.tidyverse.org) para un par de funciones que aprendiste en el @sec-functionals, y algunos operadores de funciones que aprenderás a continuación. También usaremos el [paquete memoise](https://memoise.r-lib.org) [@memoise] para el operador `memoise()`.
```{r setup}
library(purrr)
library(memoise)
```
```{=html}
<!--
### En otros idiomas
Los operadores de función se usan ampliamente en lenguajes FP como Haskell y comúnmente en Lisp, Scheme y Clojure. También son una parte importante de la programación JavaScript moderna, como en la biblioteca [underscore.js](http://underscorejs.org/). Son particularmente comunes en CoffeeScript porque su sintaxis para funciones anónimas es muy concisa. En lenguajes basados en pilas como Forth y Factor, los operadores de función se usan casi exclusivamente porque es raro referirse a las variables por su nombre. Los decoradores de Python son solo operadores de funciones con un [nombre diferente] (http://stackoverflow.com/questions/739654/). En Java, son muy raros porque es difícil manipular funciones (aunque es posible si los envuelve en objetos de tipo estrategia). También son raros en C++ porque, si bien es posible crear objetos que funcionen como funciones ("functors") sobrecargando el operador `()`, modificar estos objetos con otras funciones no es una técnica de programación común. Dicho esto, C++ 11 incluye una aplicación parcial (`std::bind`) como parte de la biblioteca estándar.
-->
```
## Operadores de funciones existentes {#sec-existing-fos}
Hay dos operadores de funciones muy útiles que lo ayudarán a resolver problemas recurrentes comunes y le darán una idea de lo que pueden hacer los operadores de funciones: `purrr::safely()` y `memoise::memoise()`.
### Captura de errores con `purrr::safely()` {#sec-safely}
\index{safely()}
\index{errors!handling}
Una ventaja de los bucles for es que si una de las iteraciones falla, aún puede acceder a todos los resultados hasta la falla:
```{r, error = TRUE}
x <- list(
c(0.512, 0.165, 0.717),
c(0.064, 0.781, 0.427),
c(0.890, 0.785, 0.495),
"oops"
)
out <- rep(NA_real_, length(x))
for (i in seq_along(x)) {
out[[i]] <- sum(x[[i]])
}
out
```
Si hace lo mismo con un funcional, no obtiene ningún resultado, lo que dificulta descubrir dónde radica el problema:
```{r, error = TRUE}
map_dbl(x, sum)
```
`purrr::safely()` proporciona una herramienta para ayudar con este problema. `safely()` es un operador de función que transforma una función para convertir errores en datos. (Puede aprender la idea básica que hace que funcione en la @sec-try-success-failure.) Comencemos echándole un vistazo fuera de `map_dbl()`:
```{r}
safe_sum <- safely(sum)
safe_sum
```
Como todos los operadores de funciones, `safely()` toma una función y devuelve una función envuelta a la que podemos llamar como de costumbre:
```{r}
str(safe_sum(x[[1]]))
str(safe_sum(x[[4]]))
```
Puedes ver que una función transformada por `safely()` siempre devuelve una lista con dos elementos, `result` y `error`. Si la función se ejecuta correctamente, `error` es `NULL` y `result` contiene el resultado; si la función falla, `result` es `NULL` y `error` contiene el error.
Ahora usemos `safely()` con un funcional:
```{r}
out <- map(x, safely(sum))
str(out)
```
La salida tiene una forma un poco inconveniente, ya que tenemos cuatro listas, cada una de las cuales es una lista que contiene el `resultado` y el `error`. Podemos hacer que la salida sea más fácil de usar girándola "al revés" con `purrr::transpose()`, de modo que obtengamos una lista de `resultados` y una lista de `errores`:
```{r}
out <- transpose(map(x, safely(sum)))
str(out)
```
Ahora podemos encontrar fácilmente los resultados que funcionaron o las entradas que fallaron:
```{r}
ok <- map_lgl(out$error, is.null)
ok
x[!ok]
out$result[ok]
```
Puedes usar esta misma técnica en muchas situaciones diferentes. Por ejemplo, imagine que está ajustando un modelo lineal generalizado (GLM) a una lista de data frames. Los GLM a veces pueden fallar debido a problemas de optimización, pero aún desea poder intentar ajustar todos los modelos y luego mirar hacia atrás a los que fallaron:
```{r, eval = FALSE}
fit_model <- function(df) {
glm(y ~ x1 + x2 * x3, data = df)
}
models <- transpose(map(datasets, safely(fit_model)))
ok <- map_lgl(models$error, is.null)
# ¿Qué datos no lograron converger?
datasets[!ok]
# ¿Qué modelos tuvieron éxito?
models[ok]
```
Creo que este es un gran ejemplo del poder de combinar funcionales y operadores de funciones: `safely()` te permite expresar de manera sucinta lo que necesitas para resolver un problema común de análisis de datos.
purrr viene con otros tres operadores de función en una línea similar:
- `possibly()`: devuelve un valor predeterminado cuando hay un error. No proporciona ninguna forma de saber si ocurrió un error o no, por lo que es mejor reservarlo para los casos en los que hay algún valor centinela obvio (como `NA`).
- `quietly()`: convierte la salida, los mensajes y los efectos secundarios de advertencia en componentes de `salida`, `mensaje` y `advertencia` de la salida.
- `auto_browse()`: ejecuta automáticamente `browser()` dentro de la función cuando hay un error.
Consulte su documentación para obtener más detalles.
### Almacenamiento en caché de cálculos con `memoise::memoise()` {#sec-memoise}
\index{memoisation} \index{Fibonacci series}
Otro operador de función útil es `memoise::memoise()`. **Memoriza** una función, lo que significa que la función recordará las entradas anteriores y devolverá los resultados almacenados en caché. La memorización es un ejemplo de la compensación clásica de las ciencias de la computación entre memoria y velocidad. Una función memorizada puede ejecutarse mucho más rápido, pero debido a que almacena todas las entradas y salidas anteriores, utiliza más memoria.
Exploremos esta idea con una función de juguete que simula una operación costosa:
```{r, cache = TRUE}
slow_function <- function(x) {
Sys.sleep(1)
x * 10 * runif(1)
}
system.time(print(slow_function(1)))
system.time(print(slow_function(1)))
```
Cuando memorizamos esta función, es lenta cuando la llamamos con nuevos argumentos. Pero cuando lo llamamos con argumentos de que se ve antes, es instantáneo: recupera el valor anterior del cómputo.
```{r, cache = TRUE}
fast_function <- memoise::memoise(slow_function)
system.time(print(fast_function(1)))
system.time(print(fast_function(1)))
```
Un uso relativamente realista de la memorización es calcular la serie de Fibonacci. La serie de Fibonacci se define recursivamente: los dos primeros valores se definen por convención, $f(0) = 0$, $f(1) = 1$, y luego $f(n) = f(n - 1) + f (n - 2)$ (para cualquier entero positivo). Una versión ingenua es lenta porque, por ejemplo, `fib(10)` calcula `fib(9)` y `fib(8)`, y `fib(9)` calcula `fib(8)` y `fib(7) )`, y así sucesivamente.
```{r}
fib <- function(n) {
if (n < 2) return(n)
fib(n - 2) + fib(n - 1)
}
system.time(fib(23))
system.time(fib(24))
```
Memorizar `fib()` hace que la implementación sea mucho más rápida porque cada valor se calcula solo una vez:
```{r}
fib2 <- memoise::memoise(function(n) {
if (n < 2) return(n)
fib2(n - 2) + fib2(n - 1)
})
system.time(fib2(23))
```
Y las llamadas futuras pueden basarse en cálculos anteriores:
```{r}
system.time(fib2(24))
```
Este es un ejemplo de **programación dinámica**, donde un problema complejo se puede dividir en muchos subproblemas superpuestos, y recordar los resultados de un subproblema mejora considerablemente el rendimiento.
Piense cuidadosamente antes de memorizar una función. Si la función no es **pura**, es decir, la salida no depende solo de la entrada, obtendrá resultados engañosos y confusos. Creé un error sutil en las herramientas de desarrollo porque memoricé los resultados de `available.packages()`, que es bastante lento porque tiene que descargar un archivo grande de CRAN. Los paquetes disponibles no cambian con tanta frecuencia, pero si tiene un proceso R que se ha estado ejecutando durante algunos días, los cambios pueden volverse importantes y, dado que el problema solo surgió en los procesos R de ejecución prolongada, el error fue muy doloroso para encontrar.
### Ejercicios
1. Base R proporciona un operador de función en forma de `Vectorize()`. ¿Qué hace? ¿Cuándo podría usarlo?
2. Lee el código fuente de `posiblemente()`. ¿Como funciona?
3. Lee el código fuente de `safely()`. ¿Como funciona?
## Estudio de caso: Creación de sus propios operadores de función {#sec-fo-case-study}
\index{loops}
`memoise()` y `safely()` son muy útiles pero también bastante complejos. En este caso de estudio, aprenderá cómo crear sus propios operadores de función más simples. Imagine que tiene un vector con nombre de URL y desea descargar cada uno en el disco. Eso es bastante simple con `walk2()` y `file.download()`:
```{r}
urls <- c(
"adv-r" = "https://adv-r.hadley.nz",
"r4ds" = "http://r4ds.had.co.nz/"
# y muchos más
)
path <- paste0(tempdir(), names(urls), ".html")
walk2(urls, path, download.file, quiet = TRUE)
```
Este enfoque está bien para un puñado de URL, pero a medida que el vector se alarga, es posible que desee agregar un par de funciones más:
- Agregue un pequeño retraso entre cada solicitud para evitar martillar el servidor.
- Mostrar un `.` cada pocas URL para que sepamos que la función sigue funcionando.
Es relativamente fácil agregar estas funciones adicionales si usamos un bucle for:
```{r, eval = FALSE}
for(i in seq_along(urls)) {
Sys.sleep(0.1)
if (i %% 10 == 0) cat(".")
download.file(urls[[i]], paths[[i]])
}
```
Creo que este ciclo for es subóptimo porque intercala diferentes preocupaciones: pausar, mostrar el progreso y descargar. Esto hace que el código sea más difícil de leer y dificulta la reutilización de los componentes en situaciones nuevas. En cambio, veamos si podemos usar operadores de función para extraer la pausa y mostrar el progreso y hacerlos reutilizables.
Primero, escribamos un operador de función que agregue un pequeño retraso. Voy a llamarlo `delay_by()` por razones que serán más claras en breve, y tiene dos argumentos: la función para envolver y la cantidad de retraso para agregar. La implementación real es bastante simple. El truco principal es forzar la evaluación de todos los argumentos como se describe en la @sec-factory-pitfalls, porque los operadores de función son un tipo especial de fábrica de funciones:
```{r}
delay_by <- function(f, amount) {
force(f)
force(amount)
function(...) {
Sys.sleep(amount)
f(...)
}
}
system.time(runif(100))
system.time(delay_by(runif, 0.1)(100))
```
Y podemos usarlo con el `walk2()` original:
```{r, eval = FALSE}
walk2(urls, path, delay_by(download.file, 0.1), quiet = TRUE)
```
Crear una función para mostrar el punto ocasional es un poco más difícil, porque ya no podemos confiar en el índice del bucle. Podríamos pasar el índice como otro argumento, pero eso rompe la encapsulación: una preocupación de la función de progreso ahora se convierte en un problema que el contenedor de nivel superior debe manejar. En su lugar, usaremos otro truco de fábrica de funciones (de la @sec-stateful-funs), para que el contenedor de progreso pueda administrar su propio contador interno:
```{r}
dot_every <- function(f, n) {
force(f)
force(n)
i <- 0
function(...) {
i <<- i + 1
if (i %% n == 0) cat(".")
f(...)
}
}
walk(1:100, runif)
walk(1:100, dot_every(runif, 10))
```
Ahora podemos expresar nuestro bucle for original como:
```{r, eval = FALSE}
walk2(
urls, path,
dot_every(delay_by(download.file, 0.1), 10),
quiet = TRUE
)
```
Esto está empezando a ser un poco difícil de leer porque estamos componiendo muchas llamadas a funciones y los argumentos se están dispersando. Una forma de resolver eso es usar la tubería:
```{r, eval = FALSE}
walk2(
urls, path,
download.file |> dot_every(10) |> delay_by(0.1),
quiet = TRUE
)
```
La canalización funciona bien aquí porque elegí cuidadosamente los nombres de las funciones para generar una oración (casi) legible: tome `download.file` luego (agregue) un punto cada 10 iteraciones, luego retrase 0.1s. Cuanto más claramente pueda expresar la intención de su código a través de nombres de funciones, más fácilmente otros (¡incluido usted en el futuro!) podrán leer y comprender el código.
### Ejercicios
1. Sopesar los pros y los contras de `download.file |> dot_every(10) |> delay_by(0.1)` versus `download.file |> delay_by(0.1) |> dot_every(10)`.
2. ¿Deberías memorizar `download.file()`? ¿Por qué o por qué no?
3. Cree un operador de función que informe cada vez que se crea o elimina un archivo en el directorio de trabajo, usando `dir()` y `setdiff()`. ¿Qué otros efectos de funciones globales le gustaría rastrear?
4. Escriba un operador de función que registre una marca de tiempo y un mensaje en un archivo cada vez que se ejecute una función.
5. Modifique `delay_by()` para que, en lugar de retrasar una cantidad de tiempo fija, asegure que haya transcurrido una cierta cantidad de tiempo desde la última vez que se llamó a la función. Es decir, si llamó a `g <- delay_by(1, f); g(); Sys.sleep(2); g()` no debería haber un retraso adicional.