---
title: "S7 basics"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{S7 basics}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
```
The S7 package provides a new OOP system designed to be a successor to S3 and S4.
It has been designed and implemented collaboratively by the RConsortium Object-Oriented Programming Working Group, which includes representatives from R-Core, BioConductor, RStudio/tidyverse, and the wider R community.
This vignette gives an overview of the most important parts of S7: classes and objects, generics and methods, and the basics of method dispatch and inheritance.
```{r setup}
library(S7)
```
## Classes and objects
S7 classes have a formal definition that you create with `new_class()`.
There are two arguments that you'll use with almost every class:
- The `name` of the class, supplied in the first argument.
- The class `properties`, the data associated with each instance of the class. The easiest way to define properties is to supply a named list where the values define the valid types of the property.
The following code defines a simple `dog` class with two properties: a character `name` and a numeric `age`.
```{r}
Dog <- new_class("Dog", properties = list(
name = class_character,
age = class_numeric
))
Dog
```
S7 provides a number of built-in definitions that allow you to refer to existing base types that are not S7 classes.
You can recognize these definitions because they all start with `class_`.
Note that I've assigned the return value of `new_class()` to an object with the same name as the class.
This is important!
That object represents the class and is what you use to construct instances of the class:
```{r}
lola <- Dog(name = "Lola", age = 11)
lola
```
Once you have an S7 object, you can get and set properties using `@`:
```{r}
lola@age <- 12
lola@age
```
S7 automatically validates the type of the property using the type supplied in `new_class()`:
```{r, error = TRUE}
lola@age <- "twelve"
```
Given an object, you can retrieves its class `S7_class()`:
```{r}
S7_class(lola)
```
S7 objects also have an S3 `class()`.
This is used for compatibility with existing S3 generics and you can learn more about it in `vignette("compatibility")`.
```{r}
class(lola)
```
If you want to learn more about the details of S7 classes and objects, including validation methods and more details of properties, please see `vignette("classes-objects")`.
## Generics and methods
S7, like S3 and S4, is built around the idea of **generic functions,** or **generics** for short.
A generic defines an interface, which uses a different implementation depending on the class of one or more arguments.
The implementation for a specific class is called a **method**, and the generic finds that appropriate method by performing **method dispatch**.
Use `new_generic()` to create a S7 generic.
In its simplest form, it only needs two arguments: the name of the generic (used in error messages) and the name of the argument used for method dispatch:
```{r}
speak <- new_generic("speak", "x")
```
Like with `new_class()`, you should always assign the result of `new_generic()` to a variable with the same name as the first argument.
Once you have a generic, you can register methods for specific classes with `method(generic, class) <- implementation`.
```{r}
method(speak, Dog) <- function(x) {
"Woof"
}
```
Once the method is registered, the generic will use it when appropriate:
```{r}
speak(lola)
```
Let's define another class, this one for cats, and define another method for `speak()`:
```{r}
Cat <- new_class("Cat", properties = list(
name = class_character,
age = class_double
))
method(speak, Cat) <- function(x) {
"Meow"
}
fluffy <- Cat(name = "Fluffy", age = 5)
speak(fluffy)
```
You get an error if you call the generic with a class that doesn't have a method:
```{r, error = TRUE}
speak(1)
```
## Method dispatch and inheritance
The `cat` and `dog` classes share the same properties, so we could use a common parent class to extract out the duplicated specification.
We first define the parent class:
```{r}
Pet <- new_class("Pet",
properties = list(
name = class_character,
age = class_numeric
)
)
```
Then use the `parent` argument to `new_class:`
```{r}
Cat <- new_class("Cat", parent = Pet)
Dog <- new_class("Dog", parent = Pet)
Cat
Dog
```
Because we have created new classes, we need to recreate the existing `lola` and `fluffy` objects:
```{r}
lola <- Dog(name = "Lola", age = 11)
fluffy <- Cat(name = "Fluffy", age = 5)
```
Method dispatch takes advantage of the hierarchy of parent classes: if a method is not defined for a class, it will try the method for the parent class, and so on until it finds a method or gives up with an error.
This inheritance is a powerful mechanism for sharing code across classes.
```{r}
describe <- new_generic("describe", "x")
method(describe, Pet) <- function(x) {
paste0(x@name, " is ", x@age, " years old")
}
describe(lola)
describe(fluffy)
method(describe, Dog) <- function(x) {
paste0(x@name, " is a ", x@age, " year old dog")
}
describe(lola)
describe(fluffy)
```
You can define a fallback method for any S7 object by registering a method for `S7_object`:
```{r}
method(describe, S7_object) <- function(x) {
"An S7 object"
}
Cocktail <- new_class("Cocktail",
properties = list(
ingredients = class_character
)
)
martini <- Cocktail(ingredients = c("gin", "vermouth"))
describe(martini)
```
Printing a generic will show you which methods are currently defined:
```{r}
describe
```
And you can use `method()` to retrieve the implementation of one of those methods:
```{r}
method(describe, Pet)
```
Learn more about method dispatch in `vignette("generics-methods")`.