15  S4

15.1 Introducción

S4 proporciona un enfoque formal para la programación orientada a objetos funcional. Las ideas subyacentes son similares a S3 (el tema del Capítulo 13), pero la implementación es mucho más estricta y utiliza funciones especializadas para crear clases (setClass()), genéricos (setGeneric()) y métodos (setMethod()). Además, S4 proporciona herencia múltiple (es decir, una clase puede tener varios padres) y envío múltiple (es decir, el envío del método puede usar la clase de varios argumentos).

Un nuevo componente importante de S4 es la ranura, un componente con nombre del objeto al que se accede mediante el operador de subconjunto especializado @. El conjunto de ranuras y sus clases forma una parte importante de la definición de una clase S4.

Estructura

  • La Sección 15.2 brinda una descripción general rápida de los componentes principales de S4: clases, genéricos y métodos.

  • La Sección 15.3 se sumerge en los detalles de las clases de S4, incluidos prototipos, constructores, ayudantes y validadores.

  • La Sección 15.4 le muestra cómo crear nuevos genéricos S4 y cómo proporcionar métodos a esos genéricos. También aprenderá acerca de las funciones de acceso que están diseñadas para permitir que los usuarios inspeccionen y modifiquen las ranuras de objetos de manera segura.

  • La Sección 15.5 se sumerge en los detalles completos del envío de métodos en S4. La idea básica es simple, pero rápidamente se vuelve más compleja una vez que se combinan la herencia múltiple y el envío múltiple.

  • La Sección 15.6 analiza la interacción entre S4 y S3 y le muestra cómo usarlos juntos.

Aprendiendo más

Al igual que los otros capítulos de OO, el enfoque aquí será cómo funciona S4, no cómo implementarlo de manera más efectiva. Si desea usarlo en la práctica, hay dos desafíos principales:

  • No hay una referencia que responda a todas sus preguntas sobre S4.

  • La documentación integrada de R a veces choca con las mejores prácticas de la comunidad.

A medida que avanza hacia un uso más avanzado, deberá reunir la información necesaria leyendo detenidamente la documentación, haciendo preguntas sobre StackOverflow y realizando experimentos. Algunas recomendaciones:

  • La comunidad de bioconductores es un usuario de S4 desde hace mucho tiempo y ha producido gran parte del mejor material sobre su uso efectivo. Comience con Clases y métodos de S4 impartido por Martin Morgan y Hervé Pagès, o busque uno más nuevo versión en [Materiales del curso de bioconductores] (https://bioconductor.org/help/course-materials/).

    Martin Morgan es un ex miembro de R-core y líder del proyecto de Bioconductor. Es un experto mundial en el uso práctico de S4 y recomiendo leer todo lo que ha escrito al respecto, comenzando con las preguntas que ha respondido en stackoverflow.

  • John Chambers es el autor del sistema S4 y proporciona una descripción general de su motivación y contexto histórico en Programación orientada a objetos, programación funcional y R (Chambers 2014). Para una exploración más completa de S4, consulte su libro Software for Data Analysis (Chambers 2008).

Requisitos previos

Todas las funciones relacionadas con S4 viven en el paquete de métodos. Este paquete siempre está disponible cuando ejecuta R de forma interactiva, pero es posible que no esté disponible cuando ejecuta R en modo por lotes, es decir, desde Rscript1. Por esta razón, es una buena idea llamar a library(methods) siempre que use S4. Esto también indica al lector que utilizará el sistema de objetos S4.

library(methods)

15.2 Lo escencial

Comenzaremos con una descripción general rápida de los componentes principales de S4. Una clase de S4 se define llamando a setClass() con el nombre de la clase y una definición de sus ranuras, y los nombres y clases de los datos de la clase:

setClass("Person", 
  slots = c(
    name = "character", 
    age = "numeric"
  )
)

Una vez que se define la clase, puede construir nuevos objetos a partir de ella llamando a new() con el nombre de la clase y un valor para cada ranura:

john <- new("Person", name = "John Smith", age = NA_real_)

Dado un objeto S4, puede ver su clase con is() y acceder a las ranuras con @ (equivalente a $) y slot() (equivalente a [[):

is(john)
#> [1] "Person"
john@name
#> [1] "John Smith"
slot(john, "age")
#> [1] NA

En general, solo debe usar @ en sus métodos. Si está trabajando con la clase de otra persona, busque funciones de accesorio que le permitan establecer y obtener valores de ranura de forma segura. Como desarrollador de una clase, también debe proporcionar sus propias funciones de acceso. Los accesores suelen ser genéricos de S4 que permiten que varias clases compartan la misma interfaz externa.

Aquí crearemos un setter y getter para el espacio age creando primero genéricos con setGeneric():

setGeneric("age", function(x) standardGeneric("age"))
setGeneric("age<-", function(x, value) standardGeneric("age<-"))

Y luego definiendo métodos con setMethod():

setMethod("age", "Person", function(x) x@age)
setMethod("age<-", "Person", function(x, value) {
  x@age <- value
  x
})

age(john) <- 50
age(john)
#> [1] 50

Si está utilizando una clase S4 definida en un paquete, puede obtener ayuda con class?Person. Para obtener ayuda para un método, coloque ? delante de una llamada (por ejemplo, ?age(john)) y ? usará la clase de los argumentos para averiguar qué archivo de ayuda necesita.

Finalmente, puede usar las funciones de sloop para identificar los objetos y genéricos de S4 que se encuentran en la naturaleza:

sloop::otype(john)
#> [1] "S4"
sloop::ftype(age)
#> [1] "S4"      "generic"

15.2.1 Ejercicios

  1. lubridate::period() devuelve una clase S4. ¿Qué ranuras tiene? ¿Qué clase es cada ranura? ¿Qué accesorios proporciona?

  2. ¿De qué otras formas puede encontrar ayuda para un método? Lea ?"?" y resuma los detalles.

15.3 Clases

Para definir una clase S4, llama a setClass() con tres argumentos:

  • La clase nombre. Por convención, los nombres de clase de S4 usan UpperCamelCase.

  • Un vector de caracteres con nombre que describe los nombres y las clases de las ranuras (campos). Por ejemplo, una persona puede estar representada por un nombre de personaje y una edad numérica: c(name = "personaje", age = "numérico"). La pseudoclase ANY permite que una ranura acepte objetos de cualquier tipo.

  • Un prototipo, una lista de valores predeterminados para cada ranura. Técnicamente, el prototipo es opcional2, pero siempre debes proporcionarlo.

El siguiente código ilustra los tres argumentos mediante la creación de una clase Person con el carácter name y las ranuras numéricas age.

setClass("Person", 
  slots = c(
    name = "character", 
    age = "numeric"
  ), 
  prototype = list(
    name = NA_character_,
    age = NA_real_
  )
)

me <- new("Person", name = "Hadley")
str(me)
#> Formal class 'Person' [package ".GlobalEnv"] with 2 slots
#>   ..@ name: chr "Hadley"
#>   ..@ age : num NA

15.3.1 Herencia

Hay otro argumento importante para setClass(): contains. Esto especifica una clase (o clases) de las que heredar las ranuras y el comportamiento. Por ejemplo, podemos crear una clase Employee que herede de la clase Person, agregando un espacio adicional que describa su boss.

setClass("Employee", 
  contains = "Person", 
  slots = c(
    boss = "Person"
  ),
  prototype = list(
    boss = new("Person")
  )
)

str(new("Employee"))
#> Formal class 'Employee' [package ".GlobalEnv"] with 3 slots
#>   ..@ boss:Formal class 'Person' [package ".GlobalEnv"] with 2 slots
#>   .. .. ..@ name: chr NA
#>   .. .. ..@ age : num NA
#>   ..@ name: chr NA
#>   ..@ age : num NA

setClass() tiene otros 9 argumentos pero están en desuso o no se recomiendan.

15.3.2 Introspección

Para determinar de qué clases hereda un objeto, usa is():

is(new("Person"))
#> [1] "Person"
is(new("Employee"))
#> [1] "Employee" "Person"

Para probar si un objeto hereda de una clase específica, usa el segundo argumento de is():

is(john, "Person")
#> [1] TRUE

15.3.3 Redefinición

En la mayoría de los lenguajes de programación, la definición de clase ocurre en tiempo de compilación y la construcción de objetos ocurre más tarde, en tiempo de ejecución. En R, sin embargo, tanto la definición como la construcción ocurren en tiempo de ejecución. Cuando llamas a setClass(), estás registrando una definición de clase en una variable global (oculta). Al igual que con todas las funciones de modificación de estado, debe usar setClass() con cuidado. Es posible crear objetos no válidos si redefine una clase después de haber creado una instancia de un objeto:

setClass("A", slots = c(x = "numeric"))
a <- new("A", x = 10)

setClass("A", slots = c(a_different_slot = "numeric"))
a
#> An object of class "A"
#> Slot "a_different_slot":
#> Error in slot(object, what): no slot of name "a_different_slot" for this object of class "A"

Esto puede causar confusión durante la creación interactiva de nuevas clases. (Las clases R6 tienen el mismo problema, como se describe en la Sección 14.2.2.)

15.3.4 Ayudante

new() es un constructor de bajo nivel adecuado para que lo use usted, el desarrollador. Las clases orientadas al usuario siempre deben combinarse con un asistente fácil de usar. Un ayudante siempre debe:

  • Tener el mismo nombre que la clase, p. myclass().

  • Tenga una interfaz de usuario cuidadosamente diseñada con valores predeterminados cuidadosamente seleccionados y conversiones útiles.

  • Cree mensajes de error cuidadosamente elaborados y adaptados a un usuario final.

  • Termine llamando a methods::new().

La clase Person es tan simple que un ayudante es casi superfluo, pero podemos usarlo para definir claramente el contrato: age es opcional pero name es obligatorio. También forzaremos la edad a un doble para que el ayudante también funcione cuando se pasa un número entero.

Person <- function(name, age = NA) {
  age <- as.double(age)
  
  new("Person", name = name, age = age)
}

Person("Hadley")
#> An object of class "Person"
#> Slot "name":
#> [1] "Hadley"
#> 
#> Slot "age":
#> [1] NA

15.3.5 Validador

El constructor verifica automáticamente que las ranuras tengan las clases correctas:

Person(mtcars)
#> Error in validObject(.Object): invalid class "Person" object: invalid object for slot "name" in class "Person": got class "data.frame", should be or extend class "character"

Deberá implementar verificaciones más complicadas (es decir, verificaciones que involucren longitudes o múltiples ranuras) usted mismo. Por ejemplo, es posible que queramos dejar en claro que la clase Person es una clase vectorial y puede almacenar datos sobre varias personas. Eso no está claro actualmente porque @name y @age pueden tener diferentes longitudes:

Person("Hadley", age = c(30, 37))
#> An object of class "Person"
#> Slot "name":
#> [1] "Hadley"
#> 
#> Slot "age":
#> [1] 30 37

Para hacer cumplir estas restricciones adicionales, escribimos un validador con setValidity(). Toma una clase y una función que devuelve TRUE si la entrada es válida y, de lo contrario, devuelve un vector de caracteres que describe los problemas:

setValidity("Person", function(object) {
  if (length(object@name) != length(object@age)) {
    "@name and @age must be same length"
  } else {
    TRUE
  }
})

Ahora ya no podemos crear un objeto no válido:

Person("Hadley", age = c(30, 37))
#> Error in validObject(.Object): invalid class "Person" object: @name and @age must be same length

NB: El método de validez solo es llamado automáticamente por new(), por lo que aún puede crear un objeto no válido modificándolo:

alex <- Person("Alex", age = 30)
alex@age <- 1:10

Puedes verificar explícitamente la validez tú mismo llamando a validObject():

validObject(alex)
#> Error in validObject(alex): invalid class "Person" object: @name and @age must be same length

En la Sección 15.4.4, usaremos validObject() para crear accesores que no pueden crear objetos no válidos.

15.3.6 Ejercicios

  1. Extiende la clase Person con campos para que coincidan con utils::person(). Piense en qué ranuras necesitará, qué clase debe tener cada ranura y qué necesitará verificar en su método de validez.

  2. ¿Qué sucede si define una nueva clase S4 que no tiene ranuras? (Sugerencia: lea acerca de las clases virtuales en ?setClass).

  3. Imagine que iba a volver a implementar factores, fechas y marcos de datos en S4. Esboce las llamadas setClass() que usaría para definir las clases. Piense en slots y prototypes apropiados.

15.4 Genéricos y métodos

El trabajo de un genérico es realizar el envío de métodos, es decir, encontrar la implementación específica para la combinación de clases pasadas al genérico. Aquí aprenderá a definir métodos y genéricos de S4; luego, en la siguiente sección, exploraremos con precisión cómo funciona el envío de métodos de S4.

Para crear un nuevo S4 genérico, llame a setGeneric() con una función que llame a standardGeneric():

setGeneric("myGeneric", function(x) standardGeneric("myGeneric"))

Por convención, los nuevos genéricos de S4 deben usar lowerCamelCase.

Es una mala práctica usar {} en el genérico, ya que desencadena un caso especial que es más costoso y, en general, es mejor evitarlo.

# Don't do this!
setGeneric("myGeneric", function(x) {
  standardGeneric("myGeneric")
})

15.4.1 Firma

Al igual que setClass(), setGeneric() tiene muchos otros argumentos. Solo hay uno que debe conocer: signature. Esto le permite controlar los argumentos que se utilizan para el envío de métodos. Si no se proporciona signature, se utilizan todos los argumentos (excepto ...). Ocasionalmente, es útil eliminar argumentos del envío. Esto le permite requerir que los métodos proporcionen argumentos como verbose = TRUE o quiet = FALSE, pero no toman parte en el envío.

setGeneric("myGeneric", 
  function(x, ..., verbose = TRUE) standardGeneric("myGeneric"),
  signature = "x"
)

15.4.2 Métodos

Un genérico no es útil sin algunos métodos, y en S4 se definen métodos con setMethod(). Hay tres argumentos importantes: el nombre del genérico, el nombre de la clase y el método en sí.

setMethod("myGeneric", "Person", function(x) {
  # method implementation
})

Más formalmente, el segundo argumento de setMethod() se llama signature. En S4, a diferencia de S3, la firma puede incluir múltiples argumentos. Esto hace que el envío de métodos en S4 sea sustancialmente más complicado, pero evita tener que implementar el envío doble como un caso especial. Hablaremos más sobre el envío múltiple en la siguiente sección. setMethod() tiene otros argumentos, pero nunca debes usarlos.

Para listar todos los métodos que pertenecen a un genérico, o que están asociados con una clase, use methods("generic") o methods(class = "class"); para encontrar la implementación de un método específico, use selectMethod("generic", "class").

15.4.3 Mostrar método

El método S4 más comúnmente definido que controla la impresión es show(), que controla cómo aparece el objeto cuando se imprime. Para definir un método para un genérico existente, primero debe determinar los argumentos. Puede obtenerlos de la documentación o mirando los args() del genérico:

args(getGeneric("show"))
#> function (object) 
#> NULL

Nuestro método show necesita tener un solo argumento object:

setMethod("show", "Person", function(object) {
  cat(is(object)[[1]], "\n",
      "  Name: ", object@name, "\n",
      "  Age:  ", object@age, "\n",
      sep = ""
  )
})
john
#> Person
#>   Name: John Smith
#>   Age:  50

15.4.4 Accesorios

Las ranuras deben considerarse un detalle de implementación interna: pueden cambiar sin previo aviso y el código de usuario debe evitar acceder a ellas directamente. En su lugar, todas las ranuras accesibles para el usuario deben ir acompañadas de un par de accesorios. Si la ranura es única para la clase, esto puede ser solo una función:

person_name <- function(x) x@name

Sin embargo, por lo general, definirá un genérico para que varias clases puedan usar la misma interfaz:

setGeneric("name", function(x) standardGeneric("name"))
setMethod("name", "Person", function(x) x@name)

name(john)
#> [1] "John Smith"

Si la ranura también se puede escribir, debe proporcionar una función de establecimiento. Siempre debe incluir validObject() en el setter para evitar que el usuario cree objetos no válidos.

setGeneric("name<-", function(x, value) standardGeneric("name<-"))
setMethod("name<-", "Person", function(x, value) {
  x@name <- value
  validObject(x)
  x
})

name(john) <- "Jon Smythe"
name(john)
#> [1] "Jon Smythe"

name(john) <- letters
#> Error in validObject(x): invalid class "Person" object: @name and @age must be same length

(Si la notación name<- no le resulta familiar, revise la Sección 6.8.)

15.4.5 Ejercicios

  1. Agregue accesores age() para la clase Person.

  2. En la definición de genérico, ¿por qué es necesario repetir dos veces el nombre del genérico?

  3. ¿Por qué el método show() definido en la Sección 15.4.3 usa is(object)[[1]]? (Sugerencia: intente imprimir la subclase de empleado).

  4. ¿Qué pasa si defines un método con nombres de argumentos diferentes al genérico?

15.5 Método de envío

El envío de S4 es complicado porque S4 tiene dos características importantes:

  • Herencia múltiple, es decir, una clase puede tener múltiples padres,
  • Envío múltiple, es decir, un genérico puede usar múltiples argumentos para elegir un método.

Estas características hacen que S4 sea muy potente, pero también pueden dificultar la comprensión de qué método se seleccionará para una determinada combinación de entradas. En la práctica, mantenga el envío de métodos lo más simple posible evitando la herencia múltiple y reservando el envío múltiple solo cuando sea absolutamente necesario.

Pero es importante describir los detalles completos, por lo que aquí comenzaremos de manera simple con herencia única y despacho único, y avanzaremos hasta los casos más complicados. Para ilustrar las ideas sin atascarse en los detalles, usaremos un gráfico de clase imaginario basado en emoji:

Emoji nos da nombres de clase muy compactos que evocan las relaciones entre las clases. Debería ser sencillo recordar que 😜 hereda de 😉 que hereda de 😶, y que 😎 hereda tanto de 🕶 como de 🙂.

15.5.1 Envío único

Comencemos con el caso más simple: una función genérica que se distribuye en una sola clase con un solo padre. El método de envío aquí es simple, por lo que es un buen lugar para definir las convenciones gráficas que usaremos para los casos más complejos.

Hay dos partes en este diagrama:

  • La parte superior, f(...), define el alcance del diagrama. Aquí tenemos un genérico con un argumento, que tiene una jerarquía de clases de tres niveles de profundidad.

  • La parte inferior es el gráfico de métodos y muestra todos los métodos posibles que podrían definirse. Los métodos que existen, es decir, que se han definido con setMethod(), tienen un fondo gris.

Para encontrar el método que se llama, comience con la clase más específica de los argumentos reales, luego siga las flechas hasta que encuentre un método que exista. Por ejemplo, si llamaste a la función con un objeto de la clase 😉, seguirías la flecha hacia la derecha para encontrar el método definido para la clase más general 😶. Si no se encuentra ningún método, el envío del método ha fallado y se genera un error. En la práctica, esto significa que siempre debe definir métodos definidos para los nodos terminales, es decir, los del extremo derecho.

Hay dos pseudoclases para las que puede definir métodos. Estas se denominan pseudoclases porque en realidad no existen, pero le permiten definir comportamientos útiles. La primera pseudoclase es ANY que coincide con cualquier clase3. Por razones técnicas que veremos más adelante, el enlace al método ANY es más largo que los enlaces entre las otras clases:

La segunda pseudoclase es MISSING. Si define un método para esta pseudoclase, coincidirá siempre que falte el argumento. No es útil para envío único, pero es importante para funciones como + y - que usan envío doble y se comportan de manera diferente dependiendo de si tienen uno o dos argumentos.

15.5.2 Herencia múltiple

Las cosas se complican más cuando la clase tiene varios padres.

El proceso básico sigue siendo el mismo: comienza desde la clase real suministrada al genérico, luego sigue las flechas hasta encontrar un método definido. El problema es que ahora hay varias flechas a seguir, por lo que es posible que encuentre varios métodos. Si eso sucede, elige el método más cercano, es decir, requiere viajar con la menor cantidad de flechas.

NB: Si bien el gráfico de métodos es una poderosa metáfora para comprender el envío de métodos, implementarlo de esta manera sería bastante ineficiente, por lo que el enfoque real que usa S4 es algo diferente. Puede leer los detalles en ?Methods_Details.

¿Qué sucede si los métodos están a la misma distancia? Por ejemplo, imagina que hemos definido métodos para 🕶 y 🙂, y llamamos al genérico 😎. Tenga en cuenta que no se puede encontrar ningún método para la clase 😶, que resaltaré con un doble contorno rojo.

Esto se llama método ambiguo, y en los diagramas lo ilustraré con un borde de puntos gruesos. Cuando esto sucede en R, recibirá una advertencia y se elegirá el método para la clase que aparece antes en el alfabeto (esto es efectivamente aleatorio y no se debe confiar en él). Cuando descubra una ambigüedad, siempre debe resolverla proporcionando un método más preciso:

El método alternativo ANY aún existe, pero las reglas son un poco más complejas. Como lo indican las líneas punteadas onduladas, el método ANY siempre se considera más lejano que un método para una clase real. Esto significa que nunca contribuirá a la ambigüedad.

Con herencias múltiples, es difícil evitar simultáneamente la ambigüedad, asegurarse de que cada método de terminal tenga una implementación y minimizar la cantidad de métodos definidos (para beneficiarse de OOP). Por ejemplo, de las seis formas de definir solo dos métodos para esta llamada, solo una está libre de problemas. Por esta razón, recomiendo utilizar la herencia múltiple con sumo cuidado: deberá pensar detenidamente en el gráfico del método y planificar en consecuencia.

15.5.3 Envío múltiple

Una vez que comprenda la herencia múltiple, comprender el envío múltiple es sencillo. Sigue varias flechas de la misma manera que antes, pero ahora cada método se especifica mediante dos clases (separadas por una coma).

No voy a mostrar ejemplos de despacho en más de dos argumentos, pero puede seguir los principios básicos para generar sus propios gráficos de métodos.

La principal diferencia entre la herencia múltiple y el envío múltiple es que hay muchas más flechas a seguir. El siguiente diagrama muestra cuatro métodos definidos que producen dos casos ambiguos:

El envío múltiple tiende a ser menos complicado para trabajar que la herencia múltiple porque generalmente hay menos combinaciones de clases de terminales. En este ejemplo, solo hay uno. Eso significa que, como mínimo, puede definir un solo método y tener un comportamiento predeterminado para todas las entradas.

15.5.4 Despacho múltiple y herencia múltiple

Por supuesto, puede combinar envío múltiple con herencia múltiple:

Un caso aún más complicado se despacha en dos clases, las cuales tienen herencia múltiple:

A medida que el gráfico del método se vuelve más y más complicado, se vuelve más y más difícil predecir qué método se llamará dada una combinación de entradas, y se vuelve más y más difícil asegurarse de que no se ha introducido ambigüedad. Si tiene que dibujar diagramas para averiguar qué método se llamará realmente, es una fuerte indicación de que debe volver atrás y simplificar su diseño.

15.5.5 Ejercicios

  1. Dibuje el gráfico del método para f(😅, 😽).

  2. Dibuje el gráfico del método para f(😃, 😉, 😙).

  3. Tome el último ejemplo que muestra envío múltiple sobre dos clases que usan herencia múltiple. ¿Qué sucede si define un método para todas las clases de terminal? ¿Por qué el método de envío no nos ahorra mucho trabajo aquí?

15.6 S4 y S3

Al escribir código S4, a menudo necesitará interactuar con clases y genéricos existentes de S3. Esta sección describe cómo las clases, los métodos y los genéricos de S4 interactúan con el código existente.

15.6.1 Clases

En slots y contains puede usar clases S4, clases S3 o la clase implícita (Sección 13.7.1) de un tipo base. Para usar una clase S3, primero debe registrarla con setOldClass(). Llame a esta función una vez para cada clase de S3, dándole el atributo de clase. Por ejemplo, la base R ya proporciona las siguientes definiciones:

setOldClass("data.frame")
setOldClass(c("ordered", "factor"))
setOldClass(c("glm", "lm"))

Sin embargo, generalmente es mejor ser más específico y proporcionar una definición completa de S4 con slots y un prototype:

setClass("factor",
  contains = "integer",
  slots = c(
    levels = "character"
  ),
  prototype = structure(
    integer(),
    levels = character()
  )
)
setOldClass("factor", S4Class = "factor")

Por lo general, estas definiciones las debe proporcionar el creador de la clase S3. Si intenta crear una clase S4 sobre una clase S3 proporcionada por un paquete, debe solicitar que el mantenedor del paquete agregue esta llamada a su paquete, en lugar de agregarla a su propio código.

Si un objeto S4 hereda de una clase S3 o un tipo base, tendrá una ranura virtual especial llamada .Data. Esto contiene el tipo base subyacente o el objeto S3:

RangedNumeric <- setClass(
  "RangedNumeric",
  contains = "numeric",
  slots = c(min = "numeric", max = "numeric"),
  prototype = structure(numeric(), min = NA_real_, max = NA_real_)
)
rn <- RangedNumeric(1:10, min = 1, max = 10)
rn@min
#> [1] 1
rn@.Data
#>  [1]  1  2  3  4  5  6  7  8  9 10

Es posible definir métodos de S3 para genéricos de S4 y métodos de S4 para genéricos de S3 (siempre que haya llamado a setOldClass()). Sin embargo, es más complicado de lo que parece a primera vista, así que asegúrese de leer detenidamente ?Methods_for_S3.

15.6.2 Genéricos

Además de crear un nuevo genérico desde cero, también es posible convertir un genérico S3 existente en un genérico S4:

setGeneric("mean")

En este caso, la función existente se convierte en el método predeterminado (ANY):

selectMethod("mean", "ANY")
#> Method Definition (Class "derivedDefaultMethod"):
#> 
#> function (x, ...) 
#> UseMethod("mean")
#> <bytecode: 0x55572ed66cb8>
#> <environment: namespace:base>
#> 
#> Signatures:
#>         x    
#> target  "ANY"
#> defined "ANY"

NB: setMethod() llamará automáticamente a setGeneric() si el primer argumento aún no es genérico, lo que le permite convertir cualquier función existente en un genérico S4. Está bien convertir un S3 genérico existente a S4, pero debe evitar convertir funciones regulares a genéricos S4 en paquetes porque eso requiere una coordinación cuidadosa si lo hacen varios paquetes.

15.6.3 Ejercicios

  1. ¿Cómo sería una definición completa de setOldClass() para un factor ordenado (es decir, agregar slots y prototype de la definición anterior)?

  2. Defina un método length para la clase Person.


  1. Esta es una peculiaridad histórica introducida porque el paquete de métodos solía tardar mucho tiempo en cargarse y Rscript está optimizado para una invocación rápida de la línea de comandos.↩︎

  2. ?setClass recomienda que evite el argumento prototype, pero esto generalmente se considera un mal consejo.↩︎

  3. La pseudoclase ANY de S4 desempeña el mismo papel que la pseudoclase default de S3.↩︎