-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathConditions.qmd
1107 lines (815 loc) · 43.5 KB
/
Conditions.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Condiciones {#sec-conditions}
```{r, include = FALSE}
source("common.R")
```
## Introducción
El sistema de **condición** proporciona un conjunto emparejado de herramientas que permiten al autor de una función indicar que algo inusual está sucediendo y al usuario de esa función manejarlo. El autor de la función **señala** las condiciones con funciones como `stop()` (para errores), `warning()` (para advertencias) y `message()` (para mensajes), luego el usuario de la función puede manejarlas. con funciones como `tryCatch()` y `withCallingHandlers()`. Comprender el sistema de condiciones es importante porque a menudo necesitará desempeñar ambos roles: señalar las condiciones de las funciones que crea y manejar las condiciones señaladas por las funciones que llama.
R ofrece un sistema de condiciones muy poderoso basado en ideas de Common Lisp. Al igual que el enfoque de R para la programación orientada a objetos, es bastante diferente a los lenguajes de programación actualmente populares, por lo que es fácil malinterpretarlo y se ha escrito relativamente poco sobre cómo usarlo de manera efectiva. Históricamente, esto ha significado que pocas personas (incluido yo mismo) han aprovechado al máximo su poder. El objetivo de este capítulo es remediar esa situación. Aquí aprenderá sobre las grandes ideas del sistema de condiciones de R, además de aprender un montón de herramientas prácticas que fortalecerán su código.
Encontré dos recursos particularmente útiles al escribir este capítulo. También puede leerlos si desea obtener más información sobre las inspiraciones y motivaciones del sistema:
- [*Un prototipo de un sistema de condiciones para R*](http://homepage.stat.uiowa.edu/~luke/R/exceptions/simpcond.html) de Robert Gentleman y Luke Tierney. Esto describe una versión anterior del sistema de condiciones de R. Si bien la implementación ha cambiado un poco desde que se escribió este documento, proporciona una buena descripción general de cómo encajan las piezas y algo de motivación para su diseño.
- [*Más allá del manejo de excepciones: condiciones y reinicios*](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html) de Peter Seibel. Esto describe el manejo de excepciones en Lisp, que resulta ser muy similar al enfoque de R. Proporciona una motivación útil y ejemplos más sofisticados. He proporcionado una traducción R del capítulo en <http://adv-r.had.co.nz/beyond-exception-handling.html>.
También encontré útil trabajar con el código C subyacente que implementa estas ideas. Si está interesado en entender cómo funciona todo, puede encontrar [mis notas](https://gist.github.com/hadley/4278d0a6d3a10e42533d59905fbed0ac) de utilidad.
### Prueba {.unnumbered}
¿Quieres saltarte este capítulo? Anímate, si puedes responder las siguientes preguntas. Encuentre las respuestas al final del capítulo en la @sec-conditions-answers.
1. ¿Cuáles son los tres tipos de condiciones más importantes?
2. ¿Qué función utiliza para ignorar los errores en el bloque de código?
3. ¿Cuál es la principal diferencia entre `tryCatch()` y `withCallingHandlers()`?
4. ¿Por qué podría querer crear un objeto de error personalizado?
### Estructura {.unnumbered}
- La @sec-signalling-conditions presenta las herramientas básicas para las condiciones de señalización y analiza cuándo es apropiado usar cada tipo.
- La @sec-ignoring-conditions le enseña sobre las herramientas más simples para manejar condiciones: funciones como `try()` y `suppressMessages()` que tragan condiciones y evitan que lleguen al nivel superior.
- La @sec-handling-conditions introduce la condición **objeto** y las dos herramientas fundamentales del manejo de condiciones: `tryCatch()` para condiciones de error y `withCallingHandlers()` para todo lo demás.
- La @sec-custom-conditions le muestra cómo ampliar los objetos de condición incorporados para almacenar datos útiles que los controladores de condiciones pueden usar para tomar decisiones más informadas.
- La @sec-condition-applications cierra el capítulo con una bolsa de sorpresas de aplicaciones prácticas basadas en las herramientas de bajo nivel que se encuentran en las secciones anteriores.
### Requisitos previos
Además de las funciones básicas de R, este capítulo utiliza funciones de señalización y manejo de condiciones de [rlang](https://rlang.r-lib.org).
```{r setup}
library(rlang)
```
## Condiciones de señalización {#sec-signalling-conditions}
\index{conditions!signalling} \index{interrupts}
Hay tres condiciones que puede señalar en el código: errores, advertencias y mensajes.
- Los errores son los más graves; indican que no hay forma de que una función continúe y la ejecución debe detenerse.
- Las advertencias se encuentran un poco entre los errores y los mensajes, y generalmente indican que algo salió mal pero la función se pudo recuperar al menos parcialmente.
- Los mensajes son los más suaves; son una forma de informar a los usuarios que se ha realizado alguna acción en su nombre.
Hay una condición final que solo se puede generar de forma interactiva: una interrupción, que indica que el usuario ha interrumpido la ejecución presionando Escape, Ctrl + Pausa o Ctrl + C (según la plataforma).
Las condiciones generalmente se muestran de manera destacada, en negrita o en color rojo, según la interfaz de R. Puede distinguirlos porque los errores siempre comienzan con "Error", las advertencias con "Warning" o "Warning message" y los mensajes sin nada.
```{r, error = TRUE}
stop("Así es como se ve un error")
warning("Así es como se ve una advertencia")
message("Así es como se ve un mensaje")
```
Las siguientes tres secciones describen errores, advertencias y mensajes con más detalle.
### Errores
\index{errors} \index{stop()} \index{abort()}
En base R, los errores son señalados, o **lanzados**, por `stop()`:
```{r, error = TRUE}
f <- function() g()
g <- function() h()
h <- function() stop("¡Esto es un error!")
f()
```
De forma predeterminada, el mensaje de error incluye la llamada, pero esto normalmente no es útil (y recapitula información que puede obtener fácilmente de `traceback()`), por lo que creo que es una buena práctica usar `call. = FALSE`[^conditions-1]:
[^conditions-1]: El final `.` en `call.` es una peculiaridad de `stop()`; no leas nada.
```{r, error = TRUE}
h <- function() stop("¡Esto es un error!", call. = FALSE)
f()
```
El rlang equivalente a `stop()`, `rlang::abort()`, hace esto automáticamente. Usaremos `abort()` a lo largo de este capítulo, pero no llegaremos a su característica más convincente, la capacidad de agregar metadatos adicionales al objeto de condición, hasta que estemos cerca del final del capítulo.
```{r, error = TRUE}
h <- function() abort("This is an error!")
f()
```
(NB: `stop()` pega varias entradas juntas, mientras que `abort()` no lo hace. Para crear mensajes de error complejos con abortar, recomiendo usar `glue::glue()`. Esto nos permite usar otros argumentos para `abortar ()` para características útiles que aprenderá en la @sec-custom-conditions.)
Los mejores mensajes de error le dicen qué está mal y le indican la dirección correcta para solucionar el problema. Escribir buenos mensajes de error es difícil porque los errores generalmente ocurren cuando el usuario tiene un modelo mental defectuoso de la función. Como desarrollador, es difícil imaginar cómo el usuario podría estar pensando incorrectamente sobre su función y, por lo tanto, es difícil escribir un mensaje que dirija al usuario en la dirección correcta. Dicho esto, la guía de estilo de tidyverse analiza algunos principios generales que hemos encontrado útiles: <http://style.tidyverse.org/error-messages.html>.
### Advertencias
\index{warnings}
Las advertencias, señaladas por `warning()`, son más débiles que los errores: indican que algo salió mal, pero el código pudo recuperarse y continuar. A diferencia de los errores, puede tener múltiples advertencias de una sola llamada de función:
```{r}
fw <- function() {
cat("1\n")
warning("W1")
cat("2\n")
warning("W2")
cat("3\n")
warning("W3")
}
```
De forma predeterminada, las advertencias se almacenan en caché y se imprimen solo cuando el control vuelve al nivel superior:
```{r, eval = FALSE}
fw()
#> 1
#> 2
#> 3
#> Warning messages:
#> 1: In f() : W1
#> 2: In f() : W2
#> 3: In f() : W3
```
\index{options!warn@\texttt{warn}} Puedes controlar este comportamiento con la opción `warn`:
- Para que las advertencias aparezcan inmediatamente, configure `options(warn = 1)`.
- Para convertir las advertencias en errores, establezca `options(warn = 2)`. Esta suele ser la forma más fácil de depurar una advertencia, ya que una vez que se trata de un error, puede usar herramientas como `traceback()` para encontrar la fuente.
- Restaurar el comportamiento predeterminado con `options(warn = 0)`.
Al igual que `stop()`, `warning()` también tiene un argumento de llamada. Es un poco más útil (ya que las advertencias a menudo están más lejos de su fuente), pero generalmente lo suprimo con `call. = FALSE`. Al igual que `rlang::abort()`, el equivalente en rlang de `warning()`, `rlang::warn()`, también suprime la `call` por defecto.
Las advertencias ocupan un lugar algo desafiante entre los mensajes ("debe saber sobre esto") y los errores ("¡debe arreglar esto!"), y es difícil dar consejos precisos sobre cuándo usarlos. En general, tenga cuidado, ya que es fácil pasar por alto las advertencias si hay muchos otros resultados y no desea que su función se recupere con demasiada facilidad de una entrada claramente no válida. En mi opinión, la base R tiende a abusar de las advertencias, y muchas advertencias en la base R estarían mejor como errores. Por ejemplo, creo que estas advertencias serían más útiles como errores:
```{r}
formals(1)
file.remove("this-file-doesn't-exist")
lag(1:3, k = 1.5)
as.numeric(c("18", "30", "50+", "345,678"))
```
Solo hay un par de casos en los que usar una advertencia es claramente apropiado:
- Cuando **desaproba** una función, desea permitir que el código anterior continúe funcionando (por lo que ignorar la advertencia está bien), pero desea alentar al usuario a cambiar a una nueva función.
- Cuando esté razonablemente seguro de que puede solucionar un problema: si estuviera 100% seguro de que podría solucionar el problema, no necesitaría ningún mensaje; si no estuviera seguro de poder solucionar correctamente el problema, arrojaría un error.
De lo contrario, use las advertencias con moderación y considere cuidadosamente si un error sería más apropiado.
### Mensajes
\index{messages} \index{cat()} \index{packageStartupMessage()}
Los mensajes, señalados por `message()`, son informativos; utilícelos para decirle al usuario que ha hecho algo en su nombre. Los buenos mensajes son un acto de equilibrio: desea proporcionar la información suficiente para que el usuario sepa lo que está sucediendo, pero no tanto como para que se sienta abrumado.
Los mensajes, `message()`, se muestran inmediatamente y no tienen un argumento `call.`:
```{r}
fm <- function() {
cat("1\n")
message("M1")
cat("2\n")
message("M2")
cat("3\n")
message("M3")
}
fm()
```
Buenos lugares para usar un mensaje son:
- Cuando un argumento predeterminado requiere una cantidad de cálculo no trivial y desea decirle al usuario qué valor se utilizó. Por ejemplo, ggplot2 informa la cantidad de contenedores utilizados si no proporciona un `binwidth`.
- En funciones que son convocadas principalmente por sus efectos secundarios que de otro modo serían silenciosos. Por ejemplo, al escribir archivos en el disco, llamar a una API web o escribir en una base de datos, es útil proporcionar mensajes de estado regulares que le informen al usuario lo que está sucediendo.
- Cuando esté a punto de iniciar un proceso de ejecución prolongada sin resultados intermedios. Una barra de progreso (por ejemplo, con [progress](https://github.com/r-lib/progress)) es mejor, pero un mensaje es un buen lugar para comenzar.
- Al escribir un paquete, a veces desea mostrar un mensaje cuando se carga su paquete (es decir, en `.onAttach()`); aquí debes usar `packageStartupMessage()`.
En general, cualquier función que produzca un mensaje debería tener alguna forma de suprimirlo, como un argumento `quiet = TRUE`. Es posible suprimir todos los mensajes con `suppressMessages()`, como aprenderá en breve, pero también es bueno dar un control más detallado.
Es importante comparar `message()` con el `cat()` estrechamente relacionado. En términos de uso y resultado, parecen bastante similares[^conditions-2]:
[^conditions-2]: Pero tenga en cuenta que `cat()` requiere un final explícito `"\n"` para imprimir una nueva línea.
```{r}
cat("Hi!\n")
message("Hi!")
```
Sin embargo, los *propósitos* de `cat()` y `message()` son diferentes. Usa `cat()` cuando el rol principal de la función es imprimir en la consola, como los métodos `print()` o `str()`. Usa `message()` como un canal lateral para imprimir en la consola cuando el propósito principal de la función es otra cosa. En otras palabras, `cat()` es para cuando el usuario *pide* que se imprima algo y `message()` es para cuando el desarrollador *elige* imprimir algo.
### Ejercicios
1. Escriba un contenedor alrededor de `file.remove()` que arroje un error si el archivo a eliminar no existe.
2. ¿Qué hace el argumento `appendLF` para `message()`? ¿Cómo se relaciona con `cat()`?
## Ignorando las condiciones {#sec-ignoring-conditions}
\index{conditions!muffling} \index{try()} \index{suppressWarnings()} \index{suppressMessages()}
La forma más sencilla de manejar las condiciones en R es simplemente ignorarlas:
- Ignora los errores con `try()`.
- Ignora las advertencias con `suppressWarnings()`.
- Ignorar mensajes con `suppressMessages()`.
Estas funciones son de mano dura, ya que no puede usarlas para suprimir un solo tipo de condición que conozca, mientras permite que pase todo lo demás. Volveremos a ese desafío más adelante en el capítulo.
`try()` permite que la ejecución continúe incluso después de que haya ocurrido un error. Normalmente, si ejecuta una función que arroja un error, finaliza inmediatamente y no devuelve un valor:
```{r, error = TRUE}
f1 <- function(x) {
log(x)
10
}
f1("x")
```
Sin embargo, si ajusta la declaración que crea el error en `try()`, se mostrará el mensaje de error [^conditions-3] pero la ejecución continuará:
[^conditions-3]: Puede suprimir el mensaje con `try(..., silent = TRUE)`.
```{r, eval = FALSE}
f2 <- function(x) {
try(log(x))
10
}
f2("a")
#> Error in log(x) : non-numeric argument to mathematical function
#> [1] 10
```
Es posible, pero no recomendado, guardar el resultado de `try()` y realizar diferentes acciones en función de si el código tuvo éxito o no [^conditions-4]. En su lugar, es mejor usar `tryCatch()` o un ayudante de nivel superior; aprenderá acerca de ellos en breve.
[^conditions-4]: Puede saber si la expresión falló porque el resultado tendrá clase `try-error`.
Un patrón simple, pero útil, es hacer una asignación dentro de la llamada: esto le permite definir un valor predeterminado que se usará si el código no funciona correctamente. Esto funciona porque el argumento se evalúa en el entorno de llamada, no dentro de la función. (Consulte la @sec-promises para obtener más detalles).
```{r, eval = FALSE}
default <- NULL
try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)
```
`suppressWarnings()` y `suppressMessages()` suprimir todas las advertencias y mensajes. A diferencia de los errores, los mensajes y advertencias no terminan la ejecución, por lo que puede haber múltiples advertencias y mensajes señalados en un solo bloque.
```{r}
suppressWarnings({
warning("Uhoh!")
warning("Otra advertencia")
1
})
suppressMessages({
message("Hola")
2
})
suppressWarnings({
message("Todavía puedes verme")
3
})
```
## Controladores de condiciones {#sec-handling-conditions}
\index{errors!catching} \index{conditions!handling} \index{tryCatch()} \index{withCallingHandlers()}
Cada condición tiene un comportamiento predeterminado: los errores detienen la ejecución y regresan al nivel superior, las advertencias se capturan y muestran en conjunto y los mensajes se muestran inmediatamente. Los **controladores** de condiciones nos permiten anular o complementar temporalmente el comportamiento predeterminado.
Dos funciones, `tryCatch()` y `withCallingHandlers()`, nos permiten registrar controladores, funciones que toman la condición señalada como único argumento. Las funciones de registro tienen la misma forma básica:
```{r, eval = FALSE}
tryCatch(
error = function(cnd) {
# código para ejecutar cuando se lanza un error
},
code_to_run_while_handlers_are_active
)
withCallingHandlers(
warning = function(cnd) {
# código para ejecutar cuando se señale una advertencia
},
message = function(cnd) {
# código para ejecutar cuando se señala el mensaje
},
code_to_run_while_handlers_are_active
)
```
Se diferencian en el tipo de controladores que crean:
- `tryCatch()` define controladores **que salen**; después de manejar la condición, el control regresa al contexto donde se llamó a `tryCatch()`. Esto hace que `tryCatch()` sea más adecuado para trabajar con errores e interrupciones, ya que estos tienen que salir de todos modos.
- `withCallingHandlers()` define controladores de **llamadas**; después de capturar la condición, el control vuelve al contexto donde se señaló la condición. Esto lo hace más adecuado para trabajar con condiciones sin error.
Pero antes de que podamos aprender y usar estos controladores, necesitamos hablar un poco sobre la condición **objetos**. Estos se crean implícitamente cada vez que señala una condición, pero se vuelven explícitos dentro del controlador.
### Objetos de condición
\index{conditions!objects} \index{catch\_cnd()}
Hasta ahora, solo hemos señalado las condiciones y no hemos mirado los objetos que se crean detrás de escena. La forma más fácil de ver un objeto de condición es atrapar uno de una condición señalada. ese es el trabajo de `rlang::catch_cnd()`:
```{r}
cnd <- catch_cnd(stop("An error"))
str(cnd)
```
Las condiciones integradas son listas con dos elementos:
- `menssage`, un vector de caracteres de longitud 1 que contiene el texto para mostrar a un usuario. Para extraer el mensaje, utilice `conditionMessage(cnd)`.
- `call`, la llamada que activó la condición. Como se describió anteriormente, no usamos la llamada, por lo que a menudo será `NULL`. Para extraerlo, usa `conditionCall(cnd)`.
Las condiciones personalizadas pueden contener otros componentes, que analizaremos en la @sec-custom-conditions.
Las condiciones también tienen un atributo `class`, lo que las convierte en objetos S3. No hablaremos de S3 hasta el @sec-s3, pero afortunadamente, incluso si no conoce S3, los objetos de condición son bastante simples. Lo más importante que debe saber es que el atributo `class` es un vector de caracteres y determina qué controladores coincidirán con la condición.
### Controladores de salida
\index{tryCatch()}
\index{handlers!exiting}
`tryCatch()` egistra los controladores existentes y, por lo general, se utiliza para controlar las condiciones de error. Le permite anular el comportamiento de error predeterminado. Por ejemplo, el siguiente código devolverá `NA` en lugar de arrojar un error:
```{r}
f3 <- function(x) {
tryCatch(
error = function(cnd) NA,
log(x)
)
}
f3("x")
```
Si no se señalan condiciones, o si la clase de la condición señalada no coincide con el nombre del controlador, el código se ejecuta normalmente:
```{r}
tryCatch(
error = function(cnd) 10,
1 + 1
)
tryCatch(
error = function(cnd) 10,
{
message("Hi!")
1 + 1
}
)
```
Los controladores configurados por `tryCatch()` se denominan controladores **exiting** porque después de señalar la condición, el control pasa al controlador y nunca vuelve al código original, lo que significa que el código sale:
```{r}
tryCatch(
message = function(cnd) "There",
{
message("Here")
stop("This code is never run!")
}
)
```
El código protegido se evalúa en el entorno de `tryCatch()`, pero el código del controlador no, porque los controladores son funciones. Es importante recordar esto si está tratando de modificar objetos en el entorno principal.
Las funciones del controlador se llaman con un solo argumento, el objeto de condición. Llamo a este argumento `cnd`, por convención. Este valor es solo moderadamente útil para las condiciones base porque contienen relativamente pocos datos. Es más útil cuando crea sus propias condiciones personalizadas, como verá en breve.
```{r}
tryCatch(
error = function(cnd) {
paste0("--", conditionMessage(cnd), "--")
},
stop("This is an error")
)
```
`tryCatch()` tiene otro argumento: `finally`. Especifica un bloque de código (no una función) para ejecutar independientemente de si la expresión inicial tiene éxito o falla. Esto puede ser útil para la limpieza, como eliminar archivos o cerrar conexiones. Esto es funcionalmente equivalente a usar `on.exit()` (y de hecho así es como se implementa), pero puede envolver fragmentos de código más pequeños que una función completa. \index{on.exit()}
```{r}
path <- tempfile()
tryCatch(
{
writeLines("Hi!", path)
# ...
},
finally = {
# always run
unlink(path)
}
)
```
### Controladores de llamadas
\index{handlers!calling} \index{withCallingHandlers()}
Los controladores configurados por `tryCatch()` se denominan controladores de salida porque hacen que el código se cierre una vez que se ha detectado la condición. Por el contrario, `withCallingHandlers()` configura controladores de **llamadas**: la ejecución del código continúa normalmente una vez que el controlador regresa. Esto tiende a hacer que `withCallingHandlers()` sea un emparejamiento más natural con las condiciones sin error. Los controladores de salida y llamada usan "controlador" en sentidos ligeramente diferentes:
- Un controlador existente maneja una señal como tú manejas un problema; hace que el problema desaparezca.
- Un controlador de llamadas maneja una señal como usted maneja un automóvil; el coche todavía existe.
Compara los resultados de `tryCatch()` y `withCallingHandlers()` en el siguiente ejemplo. Los mensajes no se imprimen en el primer caso, porque el código finaliza una vez que se completa el controlador de salida. Se imprimen en el segundo caso, porque un controlador de llamadas no sale.
```{r}
tryCatch(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
withCallingHandlers(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
```
Los controladores se aplican en orden, por lo que no debe preocuparse por quedar atrapado en un bucle infinito. En el siguiente ejemplo, el `message()` señalado por el controlador tampoco queda atrapado:
```{r}
withCallingHandlers(
message = function(cnd) message("Second message"),
message("First message")
)
```
(Pero tenga cuidado si tiene varios controladores y algunos controladores señalan condiciones que podrían ser capturadas por otro controlador: tendrá que pensar detenidamente en la orden).
El valor de retorno de un controlador de llamadas se ignora porque el código continúa ejecutándose después de que se completa el controlador; ¿Adónde iría el valor de retorno? Eso significa que los controladores de llamadas solo son útiles por sus efectos secundarios.
\index{conditions!muffling} \index{cnd\_muffle()}
Un efecto secundario importante exclusivo de los controladores de llamadas es la capacidad de **amortiguar** la señal. De forma predeterminada, una condición continuará propagándose a los controladores principales, hasta el controlador predeterminado (o un controlador existente, si se proporciona):
```{r}
# Burbujas hasta el controlador predeterminado que genera el mensaje
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
# Burbujas en tryCatch
tryCatch(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
```
Si desea evitar la condición "burbujeante" pero aún ejecuta el resto del código en el bloque, debe silenciarlo explícitamente con `rlang::cnd_muffle()`:
```{r}
# Silencia el controlador predeterminado que imprime los mensajes.
withCallingHandlers(
message = function(cnd) {
cat("Level 2\n")
cnd_muffle(cnd)
},
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
# Silencia el controlador de nivel 2 y el controlador por defecto
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) {
cat("Level 1\n")
cnd_muffle(cnd)
},
message("Hello")
)
)
```
### Pilas de llamadas
\index{call stacks} \index{cnd\_muffle()}
Para completar la sección, existen algunas diferencias importantes entre las pilas de llamadas de los controladores de salida y de llamada. Estas diferencias generalmente no son importantes, pero las incluyo aquí porque ocasionalmente las he encontrado útiles, ¡y no quiero olvidarme de ellas!
Es más fácil ver la diferencia configurando un pequeño ejemplo que usa `lobstr::cst()`:
```{r}
f <- function() g()
g <- function() h()
h <- function() message("!")
```
Los controladores de llamadas se llaman en el contexto de la llamada que señaló la condición:
```{r}
withCallingHandlers(f(), message = function(cnd) {
lobstr::cst()
cnd_muffle(cnd)
})
```
Mientras que los controladores existentes se llaman en el contexto de la llamada a `tryCatch()`:
```{r}
tryCatch(f(), message = function(cnd) lobstr::cst())
```
### Ejercicios
1. ¿Qué información adicional contiene la condición generada por `abort()` en comparación con la condición generada por `stop()`, es decir, cuál es la diferencia entre estos dos objetos? Lea la ayuda de `?abort` para obtener más información.
```{r, eval = FALSE}
catch_cnd(stop("An error"))
catch_cnd(abort("An error"))
```
2. Prediga los resultados de evaluar el siguiente código
```{r, eval = FALSE}
show_condition <- function(code) {
tryCatch(
error = function(cnd) "error",
warning = function(cnd) "warning",
message = function(cnd) "message",
{
code
NULL
}
)
}
show_condition(stop("!"))
show_condition(10)
show_condition(warning("?!"))
show_condition({
10
message("?")
warning("?!")
})
```
3. Explique los resultados de ejecutar este código:
```{r}
withCallingHandlers(
message = function(cnd) message("b"),
withCallingHandlers(
message = function(cnd) message("a"),
message("c")
)
)
```
4. Lea el código fuente de `catch_cnd()` y explique cómo funciona.
5. ¿Cómo podría reescribir `show_condition()` para usar un solo controlador?
## Condiciones personalizadas {#sec-custom-conditions}
\index{conditions!custom} \index{abort()}
Uno de los desafíos del manejo de errores en R es que la mayoría de las funciones generan una de las condiciones integradas, que contienen solo un "mensaje" y una "llamada". Eso significa que si desea detectar un tipo específico de error, solo puede trabajar con el texto del mensaje de error. Esto es propenso a errores, no solo porque el mensaje puede cambiar con el tiempo, sino también porque los mensajes se pueden traducir a otros idiomas.
Afortunadamente, R tiene una característica poderosa, pero poco utilizada: la capacidad de crear condiciones personalizadas que pueden contener metadatos adicionales. Crear condiciones personalizadas es un poco complicado en base R, pero `rlang::abort()` lo hace muy fácil ya que puede proporcionar una `.subclass` personalizada y metadatos adicionales.
El siguiente ejemplo muestra el patrón básico. Recomiendo usar la siguiente estructura de llamadas para condiciones personalizadas. Esto aprovecha la coincidencia de argumentos flexibles de R para que el nombre del tipo de error aparezca primero, seguido del texto de cara al usuario, seguido de los metadatos personalizados.
```{r, error = TRUE}
abort(
"error_not_found",
message = "Path `blah.csv` not found",
path = "blah.csv"
)
```
Las condiciones personalizadas funcionan igual que las condiciones normales cuando se usan de forma interactiva, pero permiten que los controladores hagan mucho más.
### Motivación
To explore these ideas in more depth, let's take `base::log()`. It does the minimum when throwing errors caused by invalid arguments:
```{r, error = TRUE}
log(letters)
log(1:10, base = letters)
```
Creo que podemos hacerlo mejor siendo explícitos sobre qué argumento es el problema (es decir, `x` o `base`) y diciendo cuál es la entrada problemática (no solo cuál no es).
```{r}
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
abort(paste0(
"`x` must be a numeric vector; not ", typeof(x), "."
))
}
if (!is.numeric(base)) {
abort(paste0(
"`base` must be a numeric vector; not ", typeof(base), "."
))
}
base::log(x, base = base)
}
```
Esto nos da:
```{r, error = TRUE}
my_log(letters)
my_log(1:10, base = letters)
```
Esta es una mejora para el uso interactivo, ya que es más probable que los mensajes de error guíen al usuario hacia una solución correcta. Sin embargo, no son mejores si desea manejar los errores mediante programación: todos los metadatos útiles sobre el error se atascan en una sola cadena.
### Señalización
\index{conditions!signalling}
Construyamos alguna infraestructura para mejorar esta situación. Comenzaremos proporcionando una función `abort()` personalizada para argumentos incorrectos. Esto está un poco generalizado para el ejemplo en cuestión, pero refleja patrones comunes que he visto en otras funciones. El patrón es bastante simple. Creamos un buen mensaje de error para el usuario, usando `glue::glue()`, y almacenamos metadatos en la llamada de condición para el desarrollador.
```{r}
abort_bad_argument <- function(arg, must, not = NULL) {
msg <- glue::glue("`{arg}` must {must}")
if (!is.null(not)) {
not <- typeof(not)
msg <- glue::glue("{msg}; not {not}.")
}
abort("error_bad_argument",
message = msg,
arg = arg,
must = must,
not = not
)
}
```
\newpage
::: base
Si desea generar un error personalizado sin agregar una dependencia en rlang, puede crear un objeto de condición "a mano" y luego pasarlo a `stop()`:
```{r, eval = FALSE}
stop_custom <- function(.subclass, message, call = NULL, ...) {
err <- structure(
list(
message = message,
call = call,
...
),
class = c(.subclass, "error", "condition")
)
stop(err)
}
err <- catch_cnd(
stop_custom("error_new", "This is a custom error", x = 10)
)
class(err)
err$x
```
:::
Ahora podemos reescribir `my_log()` para usar este nuevo ayudante:
```{r}
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
abort_bad_argument("x", must = "be numeric", not = x)
}
if (!is.numeric(base)) {
abort_bad_argument("base", must = "be numeric", not = base)
}
base::log(x, base = base)
}
```
`my_log()` en sí mismo no es mucho más corto, pero es un poco más significativo y asegura que los mensajes de error para argumentos incorrectos sean consistentes en todas las funciones. Produce los mismos mensajes de error interactivos que antes:
```{r, error = TRUE}
my_log(letters)
my_log(1:10, base = letters)
```
### Controlar
\index{conditions!handling}
Estos objetos de condición estructurados son mucho más fáciles de programar. El primer lugar en el que podría querer usar esta capacidad es al probar su función. Las pruebas unitarias no son un tema de este libro (consulte los [paquetes R](http://r-pkgs.had.co.nz/) para obtener más detalles), pero los conceptos básicos son fáciles de entender. El siguiente código captura el error y luego afirma que tiene la estructura que esperamos.
```{r, message = FALSE}
library(testthat)
err <- catch_cnd(my_log("a"))
expect_s3_class(err, "error_bad_argument")
expect_equal(err$arg, "x")
expect_equal(err$not, "character")
```
También podemos usar la clase (`error_bad_argument`) en `tryCatch()` para manejar solo ese error específico:
```{r}
tryCatch(
error_bad_argument = function(cnd) "bad_argument",
error = function(cnd) "other error",
my_log("a")
)
```
Cuando se usa `tryCatch()` con múltiples controladores y clases personalizadas, se llama al primer controlador que coincida con cualquier clase en el vector de clase de la señal, no a la mejor coincidencia. Por este motivo, debe asegurarse de poner primero los controladores más específicos. El siguiente código no hace lo que cabría esperar:
```{r}
tryCatch(
error = function(cnd) "other error",
error_bad_argument = function(cnd) "bad_argument",
my_log("a")
)
```
### Ejercicios
1. Dentro de un paquete, ocasionalmente es útil verificar que un paquete esté instalado antes de usarlo. Escriba una función que verifique si un paquete está instalado (con `requireNamespace("pkg", quietly = FALSE))` y, si no, arroja una condición personalizada que incluye el nombre del paquete en los metadatos.
2. Dentro de un paquete, a menudo debe detenerse con un error cuando algo no está bien. Otros paquetes que dependen de su paquete pueden verse tentados a verificar estos errores en sus pruebas unitarias. ¿Cómo podría ayudar a estos paquetes a evitar confiar en el mensaje de error que es parte de la interfaz de usuario en lugar de la API y que podría cambiar sin previo aviso?
## Aplicaciones {#sec-condition-applications}
Ahora que ha aprendido las herramientas básicas del sistema de condiciones de R, es hora de sumergirse en algunas aplicaciones. El objetivo de esta sección no es mostrar todos los usos posibles de `tryCatch()` y `withCallingHandlers()`, sino ilustrar algunos patrones comunes que surgen con frecuencia. Con suerte, esto hará que fluya su creatividad, de modo que cuando encuentre un nuevo problema, pueda encontrar una solución útil.
### Valor de falla
\index{try()}
Hay algunos patrones `tryCatch()` simples, pero útiles, basados en la devolución de un valor del controlador de errores. El caso más simple es un contenedor para devolver un valor predeterminado si ocurre un error:
```{r}
fail_with <- function(expr, value = NULL) {
tryCatch(
error = function(cnd) value,
expr
)
}
fail_with(log(10), NA_real_)
fail_with(log("x"), NA_real_)
```
Una aplicación más sofisticada es `base::try()`. A continuación, `try2()` extrae la esencia de `base::try()`; la función real es más complicada para hacer que el mensaje de error se parezca más a lo que vería si no se usara `tryCatch()`.
```{r}
try2 <- function(expr, silent = FALSE) {
tryCatch(
error = function(cnd) {
msg <- conditionMessage(cnd)
if (!silent) {
message("Error: ", msg)
}
structure(msg, class = "try-error")
},
expr
)
}
try2(1)
try2(stop("Hi"))
try2(stop("Hi"), silent = TRUE)
```
### Valores de éxito y fracaso. {#sec-try-success-failure}
Podemos ampliar este patrón para devolver un valor si el código se evalúa correctamente (`success_val`) y otro si falla (`error_val`). Este patrón solo requiere un pequeño truco: evaluar el código proporcionado por el usuario y luego `success_val`. Si el código arroja un error, nunca llegaremos a `success_val` y, en su lugar, devolveremos `error_val`.
```{r}
foo <- function(expr) {
tryCatch(
error = function(cnd) error_val,
{
expr
success_val
}
)
}
```
Podemos usar esto para determinar si una expresión falla:
```{r}
does_error <- function(expr) {
tryCatch(
error = function(cnd) TRUE,
{
expr
FALSE
}
)
}
```
O para capturar cualquier condición, como simplemente `rlang::catch_cnd()`:
```{r, eval = FALSE}
catch_cnd <- function(expr) {
tryCatch(
condition = function(cnd) cnd,
{
expr
NULL
}
)
}
```
También podemos usar este patrón para crear una variante `try()`. Un desafío con `try()` es que es un poco difícil determinar si el código tuvo éxito o falló. En lugar de devolver un objeto con una clase especial, creo que es un poco mejor devolver una lista con dos componentes `result` y `error`.
```{r}
safety <- function(expr) {
tryCatch(
error = function(cnd) {
list(result = NULL, error = cnd)
},
list(result = expr, error = NULL)
)
}
str(safety(1 + 10))
str(safety(stop("Error!")))
```
(Esto está estrechamente relacionado con `purrr::safely()`, un operador de función, al que volveremos en la @sec-safely.)
### Renuncia
\index{options!warn@\texttt{warn}}
Además de devolver valores predeterminados cuando se señala una condición, los controladores se pueden usar para generar mensajes de error más informativos. Una aplicación simple es hacer una función que funcione como `options(warn = 2)` para un solo bloque de código. La idea es simple: manejamos las advertencias lanzando un error:
```{r}
warning2error <- function(expr) {
withCallingHandlers(
warning = function(cnd) abort(conditionMessage(cnd)),
expr
)
}
```
```{r, error = TRUE}
warning2error({
x <- 2 ^ 4
warn("Hello")
})
```
Podrías escribir una función similar si estuvieras tratando de encontrar la fuente de un mensaje molesto. Más sobre esto en la @sec-non-error-failures.
````{=html}
<!--
Otro lugar común donde es útil agregar información adicional dependiente del contexto. Por ejemplo, podría tener una función para descargar datos de un sitio web remoto:
```{r}
download_data <- function(name) {
src <- paste0("http://awesomedata.com/", name, ".csv")
dst <- paste0("data/", name, ".csv")
tryCatch(
curl::curl_download(src, dst),
error = function(cnd) {
abort(
glue::glue("Failed to download remote data `{name}`"),
parent = c
)
}
)
}
```
Hay dos ideas importantes aquí:
- Reempaquetamos `curl_download()`, que descarga el archivo, para proporcionar contexto específico de nuestra función.
- Incluimos el error original como el `padre` para que el contexto original sea aun disponible.
-->
````
### Registro
Otro patrón común es registrar las condiciones para una investigación posterior. El nuevo desafío aquí es que los controladores de llamadas se llaman solo por sus efectos secundarios, por lo que no podemos devolver valores, sino que necesitamos modificar algún objeto en su lugar.
```{r}
catch_cnds <- function(expr) {
conds <- list()
add_cond <- function(cnd) {
conds <<- append(conds, list(cnd))
cnd_muffle(cnd)
}
withCallingHandlers(
message = add_cond,
warning = add_cond,
expr
)
conds
}
catch_cnds({
inform("a")
warn("b")
inform("c")
})
```
¿Qué sucede si también desea capturar errores? Deberá envolver el `withCallingHandlers()` en un `tryCatch()`. Si se produce un error, será la última condición.
```{r}
catch_cnds <- function(expr) {
conds <- list()
add_cond <- function(cnd) {
conds <<- append(conds, list(cnd))
cnd_muffle(cnd)
}
tryCatch(
error = function(cnd) {
conds <<- append(conds, list(cnd))
},
withCallingHandlers(
message = add_cond,
warning = add_cond,
expr
)
)
conds
}
catch_cnds({
inform("a")
warn("b")
abort("C")
})
```
Esta es la idea clave que subyace al paquete de evaluación [@evaluate] que impulsa a knitr: captura cada salida en una estructura de datos especial para que pueda reproducirse más tarde. En general, el paquete de evaluación es mucho más complicado que el código aquí porque también necesita manejar gráficos y salida de texto.
### Sin comportamiento predeterminado
```{=tex}
\index{signal()}
\index{cnd\_muffle()}
```
Un último patrón útil es señalar una condición que no hereda de `message`, `warning` o `error`. Debido a que no hay un comportamiento predeterminado, esto significa que la condición no tiene efecto a menos que el usuario lo solicite específicamente. Por ejemplo, podría imaginar un sistema de registro basado en condiciones:
```{r}
log <- function(message, level = c("info", "error", "fatal")) {
level <- match.arg(level)
signal(message, "log", level = level)
}
```