9.2 Systém S3

Systém S3 používá poměrně neobvyklý přístup k objektovému programování, tzv. generic function OOP. Tento přístup je jednoduchý (až primitivní), ale funguje velmi dobře a používá jej drtivá většina balíků v R. V normálních OOP jazycích patří objektu nebo třídě jak data, tak metody (funkce). V R však objektu patří jen data a jméno třídy; metody patří tzv. generické funkci. Nejjednodušší způsob, jak pochopit strukturu objektu, je nějaký objekt vytvořit.

S3 nemá formální definici tříd. Objekt se vytvoří tak, že se nějaké základní datové struktuře (většinou seznamu) nastaví atribut class na hodnotu jména třídy:

foo <- list()        # vytvoří prázdný seznam
class(foo) <- "foo"  # přiřadí mu třídu foo

Alternativně to jde provést naráz pomocí funkce structure(), která nastavuje danému objektu atributy:

foo <- structure(list(), class = "foo")

Nyní je proměnná foo objekt třídy foo:

class(foo)
## [1] "foo"

R nemá žádný mechanismus, který by kontroloval, zda je datová struktura objektu správná. Již vytvořenému objektu je možné přidat nebo ubrat datové sloty (technicky obvykle prvky seznamu) nebo změnit jeho třídu. Někdy se to hodí; nikdy to však nedělejte, pokud opravdu dobře nevíte, co děláte, jinak vznikne velmi těžko předvídatelné chování a těžko dohledatelné chyby.

Aby se zajistilo, že je struktura objektu správná, je vhodné vytvořit konstruktor, tj. funkci, která vytváří objekt. (Funkce jako numeric() jsou také konstruktory.) Ukažme si to na příkladu. Budeme chtít mít objekty třídy human, které budou obsahovat datové položky name, height a weight. Nejdříve vytvoříme konstruktor, a pak jeden objekt:

human <- function(name, height, weight)  # konstruktor
    structure(list(name = name, height = height, weight = weight),
              class = "human")
adam <- human("Adam", 173, 63)  # tvorba objektu pomocí konstruktoru human()

Dědičnost se zajistí tak, že atribut class je vektorem jmen několik tříd – zleva doprava od potomků k předkům. Budeme např. chtít mít objekty třídy manager, které jsou potomky třídy human. Opět pro ně vytvoříme konstruktor a jeden objekt:

manager <- function(name, height, weight, rank)  # konstruktor
    structure(list(name = name, height = height, weight = weight, rank = rank),
              class = c("manager", "human"))
eva <- manager("Eve", 169, 52, "CEO")  # objekt třídy manager

Eva nyní dědí vlastnosti člověka:

class(eva)
## [1] "manager" "human"
inherits(eva, "manager")
## [1] TRUE
inherits(eva, "human")
## [1] TRUE

Protože je většina objektů v S3 postavená pomocí seznamů, můžete zjistit strukturu objektu pomocí funkce str(). Hodnotu jednotlivých slotů objektů typu S3 můžete získat pomocí operátoru $:

str(eva)
## List of 4
##  $ name  : chr "Eve"
##  $ height: num 169
##  $ weight: num 52
##  $ rank  : chr "CEO"
##  - attr(*, "class")= chr [1:2] "manager" "human"
eva$rank
## [1] "CEO"

Hlavní pointa systému S3 spočívá v tom, že stejně pojmenovaná funkce volá pro každou třídu objektu jinou metodu (jinak implementovanou funkci) přesně uzpůsobenou dané třídě dat. Funkcím, které to dokážou, se říká generické funkce. Příkladem generické funkce je funkce print(), která tiskne různé informace v závislosti na tom, jaká je třída objektu, kterou chceme vytisknout. Třída human zatím nemá definovanou žádnou metodu pro generickou funkci print(), proto zavolá metodu pro seznam:

print(adam)
## $name
## [1] "Adam"
## 
## $height
## [1] 173
## 
## $weight
## [1] 63
## 
## attr(,"class")
## [1] "human"

Když vytvoříte novou třídu objektu, můžete vytvořit i novou metodu ke generické funkci tak, že vytvoříte funkci, jejíž jméno má tvar jmeno_genericke_funkce.jmeno_tridy, tj. jméno generické funkce oddělené tečkou od názvu třídy objektu. Metodu pro tisk objektů třídy human vytvoříme např. takto:

print.human <- function(x)
    cat("*** Human ***",
        paste("Name:", x$name),
        paste("Height:", x$height),
        paste("Weight:", x$weight),
        sep = "\n")
print(adam)
## *** Human ***
## Name: Adam
## Height: 173
## Weight: 63

Protože třída manager je potomkem třídy human, dědí její chování. To znamená, že pokud tato třída nemá definovanou svou metodu print(), pak zavolá metodu svého předka, třídy human:

eva  # implicitně se volá funkce print
## *** Human ***
## Name: Eve
## Height: 169
## Weight: 52

Novou generickou funkci vytvoříme tak, že vytvoříme funkci, která bude obsahovat jediný řádek UseMethod("xxx"), kde xxx je jméno nové generické funkce. Řekněme, že chceme vytvořit novou generickou funkci, která vrátí pro objekty třídy human jejich body mass index. Samozřejmě musíme vytvořit i příslušné metody. Řekněme, že budeme chtít mít různou metodu bmi() pro objekt třídy human a objekt třídy manager:

bmi <- function(x)          # generická funkce
    UseMethod("bmi")
bmi.human <- function(x)    # metoda pro třídu human
    x$weight / (x$height / 100) ^ 2
bmi.manager <- function(x)  # metoda pro třídu manager
    "classified"

Nyní můžeme zjistit, jaký mají Adam a Eva BMI (bohužel je informace o BMI manažerů tajná):

bmi(adam)
## [1] 21.04982
bmi(eva)
## [1] "classified"

Generická funkce může mít i implicitní metodu, která se použije v případě, že pro daný typ není žádná metoda k dispozici. Tato metoda se jmenuje default:

bmi.default <- function(x)
    "Unknown class"
bmi(1)
## [1] "Unknown class"

(Bez definování implicitní metody by předchozí řádek skončil chybou.)