-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathS3.qmd
889 lines (576 loc) · 39.5 KB
/
S3.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
# S3 {#sec-s3}
```{r, include = FALSE}
source("common.R")
```
## Introducción
\index{S3}
S3 es el primer y más simple sistema OO de R. S3 es informal y ad hoc, pero hay cierta elegancia en su minimalismo: no se le puede quitar ninguna parte y seguir teniendo un sistema OO útil. Por estas razones, debe usarlo, a menos que tenga una razón convincente para hacerlo de otra manera. S3 es el único sistema OO utilizado en los paquetes base y stats, y es el sistema más utilizado en los paquetes CRAN.
S3 es muy flexible, lo que significa que te permite hacer cosas que son bastante desaconsejables. Si viene de un entorno estricto como Java, esto parecerá bastante aterrador, pero le da a los programadores de R una gran libertad. Puede ser muy difícil evitar que las personas hagan algo que usted no quiere que hagan, pero sus usuarios nunca se detendrán porque hay algo que aún no ha implementado. Dado que S3 tiene pocas restricciones integradas, la clave para su uso exitoso es aplicar las restricciones usted mismo. Por lo tanto, este capítulo le enseñará las convenciones que debe seguir (casi) siempre.
El objetivo de este capítulo es mostrarle cómo funciona el sistema S3, no cómo usarlo de manera efectiva para crear nuevas clases y genéricos. Recomiendo combinar el conocimiento teórico de este capítulo con el conocimiento práctico codificado en el [paquete vctrs](https://vctrs.r-lib.org).
### Estructura {.unnumbered}
- La @sec-s3-basics brinda una descripción general rápida de todos los componentes principales de S3: clases, genéricos y métodos. También aprenderá sobre `sloop::s3_dispatch()`, que usaremos a lo largo del capítulo para explorar cómo funciona S3.
- La @sec-s3-classes entra en los detalles de la creación de una nueva clase S3, incluidas las tres funciones que deberían acompañar a la mayoría de las clases: un constructor, un ayudante y un validador.
- La @sec-s3-methods describe cómo funcionan los métodos y genéricos de S3, incluidos los aspectos básicos del envío de métodos.
- La @sec-object-styles analiza los cuatro estilos principales de los objetos de S3: vector, registro, marco de datos y escalar.
- La @sec-s3-inheritance demuestra cómo funciona la herencia en S3 y le muestra lo que necesita para hacer que una clase sea "subclasificable".
- La @sec-s3-dispatch concluye el capítulo con una discusión de los detalles más finos del envío de métodos, incluidos los tipos base, los genéricos internos, los genéricos de grupo y el envío doble.
### Requisitos previos {.unnumbered}
Las clases de S3 se implementan mediante atributos, así que asegúrese de estar familiarizado con los detalles descritos en la @sec-attributes. Usaremos vectores S3 base existentes para ejemplos y exploración, así que asegúrese de estar familiarizado con las clases factor, Date, difftime, POSIXct y POSIXlt descritas en la @sec-s3-atomic-vectors.
Usaremos el paquete [sloop](https://sloop.r-lib.org) para sus ayudantes interactivos.
```{r setup, messages = FALSE}
library(sloop)
```
## Lo esencial {#sec-s3-basics}
\index{attributes!class} \index{classes!S3} \index{class()}
Un objeto S3 es un tipo base con al menos un atributo de "clase" (se pueden usar otros atributos para almacenar otros datos). Por ejemplo, tome el factor. Su tipo base es el vector entero, tiene un atributo `clase` de "factor", y un atributo `niveles` que almacena los niveles posibles:
```{r}
f <- factor(c("a", "b", "c"))
typeof(f)
attributes(f)
```
Puede obtener el tipo base subyacente al `unclass()`, lo que elimina el atributo de clase, lo que hace que pierda su comportamiento especial:
```{r}
unclass(f)
```
\index{generics} \index{functions!generic}
Un objeto de S3 se comporta de manera diferente a su tipo base subyacente cada vez que se pasa a un **genérico** (abreviatura de función genérica). La forma más fácil de saber si una función es genérica es usar `sloop::ftype()` y buscar "genérica" en la salida:
```{r}
ftype(print)
ftype(str)
ftype(unclass)
```
Una función genérica define una interfaz, que utiliza una implementación diferente según la clase de un argumento (casi siempre el primer argumento). Muchas funciones básicas de R son genéricas, incluida la importante `print()`:
```{r}
print(f)
# la eliminación de clase vuelve al comportamiento de entero
print(unclass(f))
```
Tenga en cuenta que `str()` es genérico, y algunas clases de S3 usan ese genérico para ocultar los detalles internos. Por ejemplo, la clase `POSIXlt` que se usa para representar datos de fecha y hora en realidad está construida encima de una lista, un hecho que está oculto por su método `str()`:
```{r}
time <- strptime(c("2017-01-01", "2020-05-04 03:21"), "%Y-%m-%d")
str(time)
str(unclass(time))
```
El genérico es un intermediario: su trabajo es definir la interfaz (es decir, los argumentos) y luego encontrar la implementación correcta para el trabajo. La implementación para una clase específica se denomina **método**, y el genérico encuentra ese método realizando **despacho de métodos**.
Puede usar `sloop::s3_dispatch()` para ver el proceso de envío del método:
```{r}
s3_dispatch(print(f))
```
\index{S3!methods} Volveremos a los detalles del envío en la @sec-method-dispatch, por ahora tenga en cuenta que los métodos S3 son funciones con un esquema de nombres especial, `generic.class()`. Por ejemplo, el método `factor` para el genérico `print()` se llama `print.factor()`. Nunca debe llamar al método directamente, sino confiar en el genérico para encontrarlo por usted.
En general, puede identificar un método por la presencia de `.` en el nombre de la función, pero hay una serie de funciones importantes en base R que se escribieron antes de S3 y, por lo tanto, usan `.` para unir palabras. Si no está seguro, verifique con `sloop::ftype()`:
```{r}
ftype(t.test)
ftype(t.data.frame)
```
\index{S3!finding source} A diferencia de la mayoría de las funciones, no puede ver el código fuente de la mayoría de los métodos S3 [^s3-1] simplemente escribiendo sus nombres. Esto se debe a que los métodos de S3 generalmente no se exportan: viven solo dentro del paquete y no están disponibles en el entorno global. En su lugar, puede usar `sloop::s3_get_method()`, que funcionará independientemente de dónde resida el método:
[^s3-1]: Las excepciones son los métodos que se encuentran en el paquete base, como `t.data.frame`, y los métodos que ha creado.
```{r, error = TRUE}
weighted.mean.Date
s3_get_method(weighted.mean.Date)
```
### Ejercicios
1. Describe la diferencia entre `t.test()` y `t.data.frame()`. ¿Cuándo se llama cada función?
2. Haga una lista de las funciones básicas de R que se usan comúnmente y que contienen `.` en su nombre, pero que no son métodos de S3.
3. ¿Qué hace el método `as.data.frame.data.frame()`? ¿Por qué es confuso? ¿Cómo podría evitar esta confusión en su propio código?
4. Describa la diferencia de comportamiento en estas dos llamadas.
```{r}
set.seed(1014)
some_days <- as.Date("2017-01-31") + sample(10, 5)
mean(some_days)
mean(unclass(some_days))
```
5. ¿Qué clase de objeto devuelve el siguiente código? ¿Sobre qué tipo de base está construido? ¿Qué atributos utiliza?
```{r}
x <- ecdf(rpois(100, 10))
x
```
6. ¿Qué clase de objeto devuelve el siguiente código? ¿Sobre qué tipo de base está construido? ¿Qué atributos utiliza?
```{r}
x <- table(rpois(100, 5))
x
```
## Clases {#sec-s3-classes}
\index{S3!classes} \index{attributes!class} \index{class()}
Si ha realizado programación orientada a objetos en otros lenguajes, se sorprenderá al saber que S3 no tiene una definición formal de una clase: para convertir un objeto en una instancia de una clase, simplemente establezca el **atributo de clase**. Puedes hacerlo durante la creación con `structure()`, o después del hecho con `class<-()`:
```{r}
# Crear y asignar clases en un solo paso
x <- structure(list(), class = "my_class")
# Crear, luego establecer la clase
x <- list()
class(x) <- "my_class"
```
Puede determinar la clase de un objeto S3 con `class(x)` y ver si un objeto es una instancia de una clase usando `inherits(x, "classname")`.
```{r}
class(x)
inherits(x, "my_class")
inherits(x, "your_class")
```
El nombre de la clase puede ser cualquier cadena, pero recomiendo usar solo letras y `_`. Evite `.` porque (como se mencionó anteriormente) puede confundirse con el separador `.` entre un nombre genérico y un nombre de clase. Al usar una clase en un paquete, recomiendo incluir el nombre del paquete en el nombre de la clase. Eso asegura que no chocará accidentalmente con una clase definida por otro paquete.
S3 no tiene comprobaciones de corrección, lo que significa que puede cambiar la clase de los objetos existentes:
```{r, error = TRUE}
# Crear un modelo lineal
mod <- lm(log(mpg) ~ log(disp), data = mtcars)
class(mod)
print(mod)
# Conviértelo en una fecha (?!)
class(mod) <- "Date"
# Como era de esperar, esto no funciona muy bien
print(mod)
```
Si ha usado otros lenguajes orientados a objetos, esto podría hacerle sentir mareado, pero en la práctica esta flexibilidad causa pocos problemas. R no evita que te dispares en el pie, pero mientras no apuntes el arma a los dedos de los pies y aprietes el gatillo, no tendrás ningún problema.
Para evitar las intersecciones de pie y bala al crear su propia clase, le recomiendo que proporcione generalmente tres funciones:
- Un **constructor** de bajo nivel, `new_myclass()`, que crea eficientemente nuevos objetos con la estructura correcta.
- Un **validador**, `validate_myclass()`, que realiza verificaciones más costosas desde el punto de vista computacional para garantizar que el objeto tenga los valores correctos.
- Un **ayudante** fácil de usar, `myclass()`, que proporciona una manera conveniente para que otros creen objetos de su clase.
No necesita un validador para clases muy simples, y puede omitir el asistente si la clase es solo para uso interno, pero siempre debe proporcionar un constructor.
### Constructores {#sec-s3-constructor}
\index{S3!constructors} \index{constructors!S3}
S3 no proporciona una definición formal de una clase, por lo que no tiene una forma integrada de garantizar que todos los objetos de una clase determinada tengan la misma estructura (es decir, el mismo tipo base y los mismos atributos con los mismos tipos). En su lugar, debe aplicar una estructura coherente mediante el uso de un **constructor**.
El constructor debe seguir tres principios:
- Se llamará `new_myclass()`.
- Tener un argumento para el objeto base y uno para cada atributo.
- Comprobar el tipo del objeto base y los tipos de cada atributo.
Ilustraré estas ideas creando constructores para las clases base[^s3-2] con las que ya está familiarizado. Para comenzar, hagamos un constructor para la clase S3 más simple: `Date`. Una fecha es simplemente un doble con un único atributo: su `clase` es `Date`. Esto lo convierte en un constructor muy simple:
[^s3-2]: Las versiones recientes de R tienen constructores `.Date()`, `.difftime()`, `.POSIXct()` y `.POSIXlt()`, pero son internos, no están bien documentados y no siguen los principios que Recomiendo.
\index{Date}
```{r}
new_Date <- function(x = double()) {
stopifnot(is.double(x))
structure(x, class = "Date")
}
new_Date(c(-1, 0, 1))
```
El propósito de los constructores es ayudarte a ti, el desarrollador. Eso significa que puede mantenerlos simples y no necesita optimizar los mensajes de error para el consumo público. Si espera que los usuarios también creen objetos, debe crear una función de ayuda amigable, llamada `class_name()`, que describiré en breve.
Un constructor un poco más complicado es el de `difftime`, que se usa para representar diferencias de tiempo. Se basa de nuevo en un doble, pero tiene un atributo de `unidades` que debe tomar uno de un pequeño conjunto de valores:
\index{difftime}
```{r}
new_difftime <- function(x = double(), units = "secs") {
stopifnot(is.double(x))
units <- match.arg(units, c("secs", "mins", "hours", "days", "weeks"))
structure(x,
class = "difftime",
units = units
)
}
new_difftime(c(1, 10, 3600), "secs")
new_difftime(52, "weeks")
```
El constructor es una función de desarrollador: será llamado en muchos lugares por un usuario experimentado. Eso significa que está bien intercambiar un poco de seguridad a cambio de rendimiento, y debe evitar verificaciones potencialmente lentas en el constructor.
### Validadores
\index{S3!validators} \index{validators!S3}
Las clases más complicadas requieren controles de validez más complicados. Tome factores, por ejemplo. Un constructor solo verifica que los tipos sean correctos, lo que permite crear factores con formato incorrecto:
\index{factor}
```{r, error = TRUE}
new_factor <- function(x = integer(), levels = character()) {
stopifnot(is.integer(x))
stopifnot(is.character(levels))
structure(
x,
levels = levels,
class = "factor"
)
}
new_factor(1:5, "a")
new_factor(0:1, "a")
```
En lugar de sobrecargar al constructor con controles complicados, es mejor ponerlos en una función separada. Si lo hace, le permite crear nuevos objetos de forma económica cuando sabe que los valores son correctos y reutilizar fácilmente las comprobaciones en otros lugares.
```{r, error = TRUE}
validate_factor <- function(x) {
values <- unclass(x)
levels <- attr(x, "levels")
if (!all(!is.na(values) & values > 0)) {
stop(
"Todos los valores `x` deben ser no faltantes y mayores que cero",
call. = FALSE
)
}
if (length(levels) < max(values)) {
stop(
"Debe haber al menos tantos `levels` como valores posibles en `x`",
call. = FALSE
)
}
x
}
validate_factor(new_factor(1:5, "a"))
validate_factor(new_factor(0:1, "a"))
```
Esta función de validación se llama principalmente por sus efectos secundarios (arrojar un error si el objeto no es válido), por lo que esperaría que devuelva su entrada principal de forma invisible (como se describe en la @sec-invisible). Sin embargo, es útil que los métodos de validación regresen visiblemente, como veremos a continuación.
### Ayudantes
\index{S3!helpers} \index{helpers!S3}
Si desea que los usuarios construyan objetos de su clase, también debe proporcionar un método auxiliar que les haga la vida lo más fácil posible. Un ayudante siempre debe:
- Tener el mismo nombre que la clase, p. `myclass()`.
- Termine llamando al constructor y al validador, si existe.
- Cree mensajes de error cuidadosamente elaborados y adaptados a un usuario final.
- Tenga una interfaz de usuario cuidadosamente diseñada con valores predeterminados cuidadosamente seleccionados y conversiones útiles.
La última viñeta es la más complicada y es difícil dar consejos generales. Sin embargo, hay tres patrones comunes:
- A veces, todo lo que necesita hacer el ayudante es forzar sus entradas al tipo deseado. Por ejemplo, `new_difftime()` es muy estricto y viola la convención habitual de que puede usar un vector entero siempre que pueda usar un vector doble:
```{r, error = TRUE}
new_difftime(1:10)
```
No es el trabajo del constructor ser flexible, así que aquí creamos un ayudante que solo fuerza la entrada a un doble.
```{r}
difftime <- function(x = double(), units = "secs") {
x <- as.double(x)
new_difftime(x, units = units)
}
difftime(1:10)
```
\index{difftime}
- A menudo, la representación más natural de un objeto complejo es una cadena. Por ejemplo, es muy conveniente especificar factores con un vector de caracteres. El siguiente código muestra una versión simple de `factor()`: toma un vector de caracteres y supone que los niveles deberían ser valores únicos. Esto no siempre es correcto (ya que es posible que algunos niveles no se vean en los datos), pero es un valor predeterminado útil.
```{r, error = TRUE}
factor <- function(x = character(), levels = unique(x)) {
ind <- match(x, levels)
validate_factor(new_factor(ind, levels))
}
factor(c("a", "a", "b"))
```
\index{factor}
- Algunos objetos complejos se especifican de manera más natural mediante múltiples componentes simples. Por ejemplo, creo que es natural construir una fecha y hora proporcionando los componentes individuales (año, mes, día, etc.). Eso me lleva a este ayudante `POSIXct()` que se parece a la función existente `ISODatetime()`[^s3-3]:
```{r}
POSIXct <- function(year = integer(),
month = integer(),
day = integer(),
hour = 0L,
minute = 0L,
sec = 0,
tzone = "") {
ISOdatetime(year, month, day, hour, minute, sec, tz = tzone)
}
POSIXct(2020, 1, 1, tzone = "America/New_York")
```
\index{POSIXct}
[^s3-3]: Este ayudante no es eficiente: en segundo plano `ISODatetime()` funciona pegando los componentes en una cadena y luego usando `strptime()`. Un equivalente más eficiente está disponible en `lubridate::make_datetime()`.
Para clases más complicadas, debe sentirse libre de ir más allá de estos patrones para hacer la vida lo más fácil posible para sus usuarios.
### Ejercicios
1. Escribe un constructor para los objetos `data.frame`. ¿Sobre qué tipo base se construye un marco de datos? ¿Qué atributos utiliza? ¿Cuáles son las restricciones impuestas a los elementos individuales? ¿Qué pasa con los nombres?
2. Mejore mi ayudante `factor()` para que tenga un mejor comportamiento cuando uno o más `valores` no se encuentran en los `niveles`. ¿Qué hace `base::factor()` en esta situación?
3. Lee atentamente el código fuente de `factor()`. ¿Qué hace que mi constructor no hace?
4. Los factores tienen un atributo opcional de "contrastes". Lea la ayuda de `C()` y describa brevemente el propósito del atributo. ¿Qué tipo debe tener? Reescriba el constructor `new_factor()` para incluir este atributo.
5. Lea la documentación de `utils::as.roman()`. ¿Cómo escribirías un constructor para esta clase? ¿Necesita un validador? ¿Qué podría hacer un ayudante?
## Genéricos y métodos {#sec-s3-methods}
\index{UseMethod()}
\index{S3!generics} \index{generics!S3}
El trabajo de un genérico S3 es realizar el envío de métodos, es decir, encontrar la implementación específica para una clase. El envío de métodos se realiza mediante `UseMethod()`, al que todos los genéricos llaman[^s3-4]. `UseMethod()` toma dos argumentos: el nombre de la función genérica (obligatorio) y el argumento a usar para el envío del método (opcional). Si omite el segundo argumento, se enviará en función del primer argumento, que casi siempre es lo que se desea.
[^s3-4]: La excepción son los genéricos internos, que se implementan en C y son el tema de la @sec-internal-generics.
La mayoría de los genéricos son muy simples y consisten solo en una llamada a `UseMethod()`. Tome `mean()` por ejemplo:
```{r}
mean
```
Crear su propio genérico es igualmente simple:
```{r}
my_new_generic <- function(x) {
UseMethod("my_new_generic")
}
```
(Si se pregunta por qué tenemos que repetir `my_new_generic` dos veces, piense en la @sec-first-class-functions.)
No pasa ninguno de los argumentos del genérico a `UseMethod()`; utiliza magia profunda para pasar al método automáticamente. El proceso preciso es complicado y con frecuencia sorprendente, por lo que debe evitar realizar cualquier cálculo de forma genérica. Para conocer todos los detalles, lea detenidamente la sección Detalles técnicos en `?UseMethod`.
### Método de envío {#sec-method-dispatch}
\index{S3!method dispatch} \index{method dispatch!S3}
¿Cómo funciona `UseMethod()`? Básicamente, crea un vector de nombres de métodos, `paste0("generic", ".", c(class(x), "default"))`, y luego busca cada método potencial a su vez. Podemos ver esto en acción con `sloop::s3_dispatch()`. Le das una llamada a un genérico S3 y enumera todos los métodos posibles. Por ejemplo, ¿qué método se llama cuando imprime un objeto `Date`?
```{r}
x <- Sys.Date()
s3_dispatch(print(x))
```
La salida aquí es simple:
- `=>` indica el método que se llama, aquí `print.Date()`
- `*` indica un método que está definido, pero no llamado, aquí `print.default()`.
La clase "predeterminada" es una **pseudoclase** especial. Esta no es una clase real, pero se incluye para que sea posible definir un respaldo estándar que se encuentra siempre que un método específico de clase no está disponible.
La esencia del envío de métodos es bastante simple, pero a medida que avanza el capítulo, verá que se vuelve progresivamente más complicado para abarcar la herencia, los tipos base, los genéricos internos y los genéricos de grupo. El siguiente código muestra un par de casos más complicados a los que volveremos en las secciones, @sec-s3-inheritance y @sec-s3-dispatch.
```{r}
x <- matrix(1:10, nrow = 2)
s3_dispatch(mean(x))
s3_dispatch(sum(Sys.time()))
```
### Encontrar métodos
\index{S3!methods!locating}
`sloop::s3_dispatch()` te permite encontrar el método específico usado para una sola llamada. ¿Qué sucede si desea encontrar todos los métodos definidos para un genérico o asociados con una clase? Ese es el trabajo de `sloop::s3_methods_generic()` y `sloop::s3_methods_class()`:
```{r}
s3_methods_generic("mean")
s3_methods_class("ordered")
```
### Crear métodos {#sec-s3-arguments}
\index{S3!methods!creating} \index{methods!S3}
Hay dos arrugas a tener en cuenta cuando crea un nuevo método:
- Primero, solo debe escribir un método si posee el genérico o la clase. R le permitirá definir un método incluso si no lo hace, pero es de muy mala educación. En su lugar, trabaje con el autor del genérico o de la clase para agregar el método en su código.
- Un método debe tener los mismos argumentos que su genérico. Esto se aplica en los paquetes mediante `R CMD check`, pero es una buena práctica incluso si no está creando un paquete.
Hay una excepción a esta regla: si el genérico tiene `...`, el método puede contener un superconjunto de argumentos. Esto permite que los métodos tomen argumentos adicionales arbitrarios. La desventaja de usar `...`, sin embargo, es que cualquier argumento mal escrito se tragará silenciosamente [^s3-5], como se menciona en la @sec-fun-dot-dot-dot.
[^s3-5]: Consulte <https://github.com/hadley/ellipsis> para ver una forma experimental de advertir cuando los métodos no usan todos los argumentos en `...`, lo que proporciona una posible resolución de este problema.
### Ejercicios
1. Lea el código fuente de `t()` y `t.test()` y confirme que `t.test()` es un método genérico de S3 y no un método de S3. ¿Qué pasa si creas un objeto con la clase `test` y llamas `t()` con él? ¿Por qué?
```{r, results = FALSE}
x <- structure(1:10, class = "test")
t(x)
```
2. ¿Para qué genéricos tiene métodos la clase `table`?
3. ¿Para qué genéricos tiene métodos la clase `ecdf`?
4. ¿Qué base genérica tiene el mayor número de métodos definidos?
5. Lea detenidamente la documentación de `UseMethod()` y explique por qué el siguiente código devuelve los resultados que devuelve. ¿Qué dos reglas usuales de evaluación de funciones viola `UseMethod()`?
```{r}
g <- function(x) {
x <- 10
y <- 10
UseMethod("g")
}
g.default <- function(x) c(x = x, y = y)
x <- 1
y <- 1
g(x)
```
6. ¿Cuáles son los argumentos para `[`? ¿Por qué es una pregunta difícil de responder?
## Estilos de objeto {#sec-object-styles}
\index{S3!object styles}
Hasta ahora me he centrado en clases de estilo vectorial como `Date` y `factor`. Estos tienen la propiedad clave de que `length(x)` representa el número de observaciones en el vector. Hay tres variantes que no tienen esta propiedad:
- Los objetos de estilo de registro utilizan una lista de vectores de igual longitud para representar componentes individuales del objeto. El mejor ejemplo de esto es `POSIXlt`, que debajo del capó es una lista de 11 componentes de fecha y hora como año, mes y día. Las clases de estilo de registro anulan `longitud()` y los métodos de creación de subconjuntos para ocultar este detalle de implementación.
```{r}
x <- as.POSIXlt(ISOdatetime(2020, 1, 1, 0, 0, 1:3))
x
length(x)
length(unclass(x))
x[[1]] # the first date time
unclass(x)[[1]] # the first component, the number of seconds
```
\index{POSIXlt}
- Los marcos de datos son similares a los objetos de estilo de registro en que ambos usan listas de vectores de igual longitud. Sin embargo, los marcos de datos son conceptualmente bidimensionales y los componentes individuales se exponen fácilmente al usuario. El número de observaciones es el número de filas, no la longitud:
```{r}
x <- data.frame(x = 1:100, y = 1:100)
length(x)
nrow(x)
```
\index{Date}
- Los objetos escalares normalmente usan una lista para representar una sola cosa. Por ejemplo, un objeto `lm` es una lista de longitud 12 pero representa un modelo.
```{r}
mod <- lm(mpg ~ wt, data = mtcars)
length(mod)
```
Los objetos escalares también se pueden construir sobre funciones, llamadas y entornos[^s3-6]. En general, esto es menos útil, pero puede ver aplicaciones en `stats::ecdf()`, R6 (@sec-r6) y `rlang::quo()` (@sec-quasiquotation) . \index{lm()}
[^s3-6]: También puede construir un objeto encima de una lista de pares, pero todavía tengo que encontrar una buena razón para hacerlo.
Desafortunadamente, describir el uso apropiado de cada uno de estos estilos de objeto está más allá del alcance de este libro. Sin embargo, puede obtener más información en la documentación del paquete vctrs (<https://vctrs.r-lib.org>); el paquete también proporciona constructores y ayudantes que facilitan la implementación de los diferentes estilos.
### Ejercicios
1. Categorice los objetos devueltos por `lm()`, `factor()`, `table()`, `as.Date()`, `as.POSIXct()` `ecdf()`, `ordered()`, `I()` en los estilos descritos anteriormente.
2. ¿Cómo sería una función constructora para objetos `lm`, `new_lm()`? Use `?lm` y experimente para descubrir los campos obligatorios y sus tipos.
## Herencia {#sec-s3-inheritance}
\index{S3!inheritance} \index{S3!methods!inheriting} \index{inheritance!S3}
Las clases de S3 pueden compartir el comportamiento a través de un mecanismo llamado **herencia**. La herencia está impulsada por tres ideas:
- La clase puede ser un carácter *vector*. Por ejemplo, las clases `ordered` y `POSIXct` tienen dos componentes en su clase:
```{r}
class(ordered("x"))
class(Sys.time())
```
\index{POSIXct}
- Si no se encuentra un método para la clase en el primer elemento del vector, R busca un método para la segunda clase (y así sucesivamente):
```{r}
s3_dispatch(print(ordered("x")))
s3_dispatch(print(Sys.time()))
```
- Un método puede delegar trabajo llamando a `NextMethod()`. Volveremos a eso muy pronto; por ahora, tenga en cuenta que `s3_dispatch()` informa delegación con `->`.
```{r}
s3_dispatch(ordered("x")[1])
s3_dispatch(Sys.time()[1])
```
Antes de continuar, necesitamos un poco de vocabulario para describir la relación entre las clases que aparecen juntas en un vector de clase. Diremos que `ordered` es una **subclase** de `factor` porque siempre aparece antes que él en el vector de clase y, a la inversa, diremos que `factor` es una **superclase** de `ordered`.
S3 no impone restricciones en la relación entre subclases y superclases, pero su vida será más fácil si impone algunas. Le recomiendo que se adhiera a dos principios simples al crear una subclase:
- El tipo base de la subclase debe ser el mismo que el de la superclase.
- Los atributos de la subclase deben ser un superconjunto de los atributos de la superclase.
`POSIXt` no se adhiere a estos principios porque `POSIXct` tiene tipo doble y `POSIXlt` tiene tipo lista. Esto significa que `POSIXt` no es una superclase, e ilustra que es bastante posible usar el sistema de herencia S3 para implementar otros estilos de código compartido (aquí `POSIXt` juega un papel más como una interfaz), pero necesitará descubra convenciones seguras usted mismo. \index{POSIXt}
### `NextMethod()`
\index{NextMethod()}
`NextMethod()` es la parte más difícil de entender de la herencia, por lo que comenzaremos con un ejemplo concreto para el caso de uso más común: `[`. Comenzaremos creando una clase de juguete simple: una clase `secreta` que oculta su salida cuando se imprime:
```{r}
new_secret <- function(x = double()) {
stopifnot(is.double(x))
structure(x, class = "secret")
}
print.secret <- function(x, ...) {
print(strrep("x", nchar(x)))
invisible(x)
}
x <- new_secret(c(15, 1, 456))
x
```
Esto funciona, pero el método predeterminado `[` no conserva la clase:
```{r}
s3_dispatch(x[1])
x[1]
```
Para arreglar esto, necesitamos proporcionar un método `[.secret`. ¿Cómo podríamos implementar este método? El enfoque ingenuo no funcionará porque nos quedaremos atrapados en un bucle infinito:
```{r}
`[.secret` <- function(x, i) {
new_secret(x[i])
}
```
En su lugar, necesitamos alguna forma de llamar al código `[` subyacente, es decir, la implementación que sería llamada si no tuviéramos un método `[.secret`. Un enfoque sería `unclass()` el objeto:
```{r}
`[.secret` <- function(x, i) {
x <- unclass(x)
new_secret(x[i])
}
x[1]
```
Esto funciona, pero es ineficiente porque crea una copia de `x`. Un mejor enfoque es usar `NextMethod()`, que resuelve de manera concisa el problema de delegar al método que se habría llamado si `[.secret` no existiera:
```{r}
`[.secret` <- function(x, i) {
new_secret(NextMethod())
}
x[1]
```
Podemos ver lo que está pasando con `sloop::s3_dispatch()`:
```{r}
s3_dispatch(x[1])
```
El `=>` indica que se llama a `[.secret`, pero que `NextMethod()` delega el trabajo al método interno subyacente `[`, como se muestra en `->`.
Al igual que con `UseMethod()`, la semántica precisa de `NextMethod()` es compleja. En particular, realiza un seguimiento de la lista de posibles métodos siguientes con una variable especial, lo que significa que la modificación del objeto que se envía no tendrá ningún impacto en el método que se llamará a continuación.
### Permitir subclases {#sec-s3-subclassing}
\index{S3!subclassing}
Cuando crea una clase, debe decidir si desea permitir subclases, ya que requiere algunos cambios en el constructor y una reflexión cuidadosa en sus métodos.
Para permitir subclases, el constructor principal debe tener argumentos `...` y `class`:
```{r}
new_secret <- function(x, ..., class = character()) {
stopifnot(is.double(x))
structure(
x,
...,
class = c(class, "secret")
)
}
```
Luego, el constructor de la subclase puede simplemente llamar al constructor de la clase principal con argumentos adicionales según sea necesario. Por ejemplo, imagina que queremos crear una clase supersecreta que también oculta la cantidad de caracteres:
```{r}
new_supersecret <- function(x) {
new_secret(x, class = "supersecret")
}
print.supersecret <- function(x, ...) {
print(rep("xxxxx", length(x)))
invisible(x)
}
x2 <- new_supersecret(c(15, 1, 456))
x2
```
Para permitir la herencia, también debe pensar detenidamente en sus métodos, ya que ya no puede usar el constructor. Si lo hace, el método siempre devolverá la misma clase, independientemente de la entrada. Esto obliga a quien hace una subclase a hacer mucho trabajo extra.
Concretamente, esto significa que debemos revisar el método `[.secret`. Actualmente siempre devuelve un `secret()`, incluso cuando se le da un supersecreto:
```{r}
`[.secret` <- function(x, ...) {
new_secret(NextMethod())
}
x2[1:3]
```
\index{vec\_restore()}
Queremos asegurarnos de que `[.secret` devuelva la misma clase que `x` incluso si es una subclase. Por lo que puedo decir, no hay forma de resolver este problema usando solo la base R. En su lugar, deberá utilizar el paquete vctrs, que proporciona una solución en forma de `vctrs::vec_restore()` genérico. Este genérico toma dos entradas: un objeto que ha perdido información de subclase y un objeto de plantilla para usar para la restauración.
Por lo general, los métodos `vec_restore()` son bastante simples: simplemente llama al constructor con los argumentos apropiados:
```{r}
vec_restore.secret <- function(x, to, ...) new_secret(x)
vec_restore.supersecret <- function(x, to, ...) new_supersecret(x)
```
(Si su clase tiene atributos, deberá pasarlos de `to` al constructor).
Ahora podemos usar `vec_restore()` en el método `[.secret`:
```{r}
`[.secret` <- function(x, ...) {
vctrs::vec_restore(NextMethod(), x)
}
x2[1:3]
```
(Solo entendí completamente este problema recientemente, por lo que al momento de escribir no se usa en el tidyverse. Con suerte, para cuando estés leyendo esto, se habrá implementado, lo que hará que sea mucho más fácil (por ejemplo) subclasificar tibbles. )
Si construye su clase usando las herramientas provistas por el paquete vctrs, `[` obtendrá este comportamiento automáticamente. Solo necesitará proporcionar su propio método `[` si usa atributos que dependen de los datos o desea un comportamiento de subconjunto no estándar. Ver `?vctrs::new_vctr` para más detalles.
### Ejercicios
1. ¿Cómo admite subclases `[.Date`? ¿Cómo no admite subclases?
2. R tiene dos clases para representar datos de fecha y hora, `POSIXct` y `POSIXlt`, que heredan ambas de `POSIXt`. ¿Qué genéricos tienen comportamientos diferentes para las dos clases? ¿Qué genéricos comparten el mismo comportamiento?
3. ¿Qué espera que devuelva este código? ¿Qué devuelve realmente? ¿Por qué?
```{r, eval = FALSE}
generic2 <- function(x) UseMethod("generic2")
generic2.a1 <- function(x) "a1"
generic2.a2 <- function(x) "a2"
generic2.b <- function(x) {
class(x) <- "a1"
NextMethod()
}
generic2(structure(list(), class = c("b", "a2")))
```
## Detalles de envío {#sec-s3-dispatch}
\index{S3!method dispatch}
Este capítulo concluye con algunos detalles adicionales sobre el envío de métodos. Es seguro omitir estos detalles si es nuevo en S3.
### S3 y tipos básicos {#sec-implicit-class}
\index{implicit class} \index{S3!implicit class}
¿Qué sucede cuando llama a un genérico S3 con un objeto base, es decir, un objeto sin clase? Podrías pensar que enviaría lo que `class()` devuelve:
```{r}
class(matrix(1:5))
```
Pero, lamentablemente, el envío se produce en la **clase implícita**, que tiene tres componentes:
- La cadena "array" o "matrix" si el objeto tiene dimensiones
- El resultado de `typeof()` con algunos ajustes menores
- La cadena "numeric" si el objeto es "integer" o "double"
No hay una función base que calcule la clase implícita, pero puede usar `sloop::s3_class()`
```{r}
s3_class(matrix(1:5))
```
Esto es usado por `s3_dispatch()`:
```{r}
s3_dispatch(print(matrix(1:5)))
```
Esto significa que la clade, `class()`, de un objeto no determina de forma única su envío:
```{r}
x1 <- 1:5
class(x1)
s3_dispatch(mean(x1))
x2 <- structure(x1, class = "integer")
class(x2)
s3_dispatch(mean(x2))
```
### Genéricos internos {#sec-internal-generics}
\index{generics!internal}
Algunas funciones básicas, como `[`, `sum()` y `cbind()`, se denominan **genéricos internos** porque no llaman a `UseMethod()` sino que llaman a las funciones de C `DispatchGroup( )` o `DispatchOrEval()`. `s3_dispatch()` muestra genéricos internos al incluir el nombre del genérico seguido de `(internal)`:
```{r}
s3_dispatch(Sys.time()[1])
```
Por motivos de rendimiento, los genéricos internos no envían a los métodos a menos que se haya establecido el atributo de clase, lo que significa que los genéricos internos no utilizan la clase implícita. Nuevamente, si alguna vez se siente confundido acerca del envío de métodos, puede confiar en `s3_dispatch()`.
### Genéricos del grupo
\index{S3!group generics} \index{generics!group}
Los genéricos de grupo son la parte más complicada del envío de métodos de S3 porque involucran tanto `NextMethod()` como genéricos internos. Al igual que los genéricos internos, solo existen en la base R y no puede definir su propio grupo genérico.
Hay cuatro genéricos de grupo:
- **Matemáticas**: `abs()`, `sign()`, `sqrt()`, `floor()`, `cos()`, `sin()`, `log()`, y más (ver `?Math` para la lista completa).
- **Operaciones**: `+`, `-`, `*`, `/`, `^`, `%%`, `%/%`, `&`, `|`, `!`, `==`, `!=`, `<`, `<=`, `>=`, y `>`.
- **Resumen**: `all()`, `any()`, `sum()`, `prod()`, `min()`, `max()`, y `range()`.
- **Complejo**: `Arg()`, `Conj()`, `Im()`, `Mod()`, `Re()`.
La definición de un solo grupo genérico para su clase anula el comportamiento predeterminado para todos los miembros del grupo. Los métodos para genéricos grupales se buscan solo si los métodos para el genérico específico no existen:
```{r}
s3_dispatch(sum(Sys.time()))
```
La mayoría de los genéricos de grupo implican una llamada a `NextMethod()`. Por ejemplo, tome los objetos `difftime()`. Si observa el envío del método para `abs()`, verá que hay un grupo genérico `Math` definido.
```{r}
y <- as.difftime(10, units = "mins")
s3_dispatch(abs(y))
```
`Math.difftime` básicamente se ve así:
```{r}
Math.difftime <- function(x, ...) {
new_difftime(NextMethod(), units = attr(x, "units"))
}
```
Despacha al siguiente método, aquí el valor predeterminado interno, para realizar el cálculo real y luego restaurar la clase y los atributos. (Para admitir mejor las subclases de `difftime`, sería necesario llamar a `vec_restore()`, como se describe en la @sec-s3-subclassing.)
Dentro de una función genérica de grupo, una variable especial `.Generic` proporciona la función genérica real llamada. Esto puede ser útil cuando se producen mensajes de error y, a veces, puede ser útil si necesita recuperar manualmente el genérico con diferentes argumentos.
### Despacho doble
\index{double dispatch} \index{method dispatch!S3!double dispatch}
Los genéricos del grupo Ops, que incluye la aritmética de dos argumentos y los operadores booleanos como `-` y `&`, implementan un tipo especial de envío de métodos. Despachan en el tipo de *ambos* argumentos, que se llama **despacho doble**. Esto es necesario para preservar la propiedad conmutativa de muchos operadores, es decir, `a + b` debería ser igual a `b + a`. Tome el siguiente ejemplo simple:
```{r}
date <- as.Date("2017-01-01")
integer <- 1L
date + integer
integer + date
```
Si `+` se enviara solo en el primer argumento, devolvería valores diferentes para los dos casos. Para superar este problema, los genéricos del grupo Ops utilizan una estrategia ligeramente diferente a la habitual. En lugar de hacer un envío de un solo método, hacen dos, uno para cada entrada. Hay tres posibles resultados de esta búsqueda:
- Los métodos son los mismos, por lo que no importa qué método se utilice.
- Los métodos son diferentes y R recurre al método interno con una advertencia.
- Un método es interno, en cuyo caso R llama al otro método.
Este enfoque es propenso a errores, por lo que si desea implementar un despacho doble robusto para operadores algebraicos, le recomiendo usar el paquete vctrs. Ver `?vctrs::vec_arith` para más detalles.
### Ejercicios
1. Explique las diferencias en el envío a continuación:
```{r}
length.integer <- function(x) 10
x1 <- 1:5
class(x1)
s3_dispatch(length(x1))
x2 <- structure(x1, class = "integer")
class(x2)
s3_dispatch(length(x2))
```
2. ¿Qué clases tienen un método para el grupo `Math` genérico en base R? Lee el código fuente. ¿Cómo funcionan los métodos?
3. `Math.difftime()` es más complicado de lo que describí. ¿Por qué?