B Programação Orientada a Objetos

Como vimos anteriormente, o R é uma linguagem de programação orientada à objetos. Dois conceitos fundamentais desse tipo de linguagem são os de classe e método. Já vimos também que todo objeto no R possui uma classe (que define sua estrutura) e analisamos algumas delas. O que seria então um método? Para responder essa pergunta precisamos entender inicialmente os tipos de orientação a objetos que o R possui.

O R possui 3 sitemas de orientação a objetos: S3, S4, e RC:

  • S3: implementa um estilo de programação orientada a objeto chamada de generic-function. Esse é o estilo mais básico de programação em R (e também o mais utilizado). A ideia é que existam funções genéricas que decidem qual método aplicar de acordo com a classe do objeto. Os métodos são definidos da mesma forma que qualquer função, mas chamados de maneira diferente. É um estilo de programação mais “informal”, mas possibilita uma grande liberdade para o programador.
  • S4: é um estilo mais formal, no sentido que que as funções genéricas devem possuir uma classe formal definida. Além disso, é possível também fazer o despacho múltiplo de métodos, ao contrário da classe S3.
  • RC: (Reference Classes, antes chamado de R5) é o sistema mais novo implementado no R. A principal diferença com os sistemas S3 e S4 é que métodos pertencem à objetos, não à funções. Isso faz com que objetos da classe RC se comportem mais como objetos da maioria das linguagens de programação, como Python, Java, e C#.

Nesta sessão vamos abordar como funcionam os métodos como definidos pelo sistema S3, por ser o mais utilizado na prática para se criar novas funções no R. Para saber mais sobre os outros métodos, consulte o livro Advanced R.

Vamos entender como uma função genérica pode ser criada através de um exemplo. Usando a função methods(), podemos verificar quais métodos estão disponíveis para uma determinada função, por exemplo, para a função mean():

methods(mean)
[1] mean.Date        mean.default     mean.difftime    mean.POSIXct    
[5] mean.POSIXlt     mean.quosure*    mean.vctrs_vctr*
see '?methods' for accessing help and source code

O resultado são expressões do tipo mean.<classe>, onde <classe> é uma classe de objeto como aquelas vistas anteriormente. Isso significa que a função mean(), quando aplicada à um objeto da classe Date, por exemplo, pode ter um comportamento diferente quando a mesma função for aplicada à um objeto de outra classe (numérica).

Suponha que temos o seguinte vetor numérico:

set.seed(1)
vec <- rnorm(100)
class(vec)
[1] "numeric"

e queremos calcular sua média. Basta aplicar a função mean() nesse objeto para obtermos o resultado esperado

mean(vec)
[1] 0.1088874

Mas isso só é possível porque existe um método definido espcificamente para um vetor da classe numeric, que nesse caso é a função mean.default. A função genérica nesse caso é a mean(), e a função método é a mean.default. Veja que não precisamos escrever o nome inteiro da função genérica para que ela seja utilizada, como por exemplo,

mean.default(vec)
[1] 0.1088874

Uma vez passado um objeto para uma função, é a classe do objeto que irá definir qual método utilizar, de acordo com os métodos disponíveis. Veja o que acontece se forçarmos o uso da função mean.Date() nesse vetor

mean.Date(vec)
[1] "1970-01-01"

O resultado não faz sentido pois ele é específico para um objeto da classe Date.

Pode-se ainda alterar a classe de um objeto, que passa a ser tratado pelo método associado a esta classe, se houver.

class(vec) <- "Date"
mean(vec)
[1] "1970-01-01"

Em S3 um objeto pode ter mais de uma classe e neste caso a procura por método segue a ordem de especificação. Procura-se, sequencialmente, métodos para todos as classes definidas. Veja esta sequência de três atribuições de classes.

class(vec) <- c("numeric", "Date")
mean(vec)
[1] "1970-01-01"
class(vec) <- c("Date", "numeric")
mean(vec)
[1] "1970-01-01"
class(vec) <- c("foo", "Date")
mean(vec)
[1] "1970-01-01"

Em todos casos foi processada o método mean.Date(). Na primeira e terceira porque não existe método específico para classes numeric ou foo. Na segunda porque Date era a primeira classe do objeto para qual há um método específico.

Tudo isso acontece por causa de um mecanismo chamado de despacho de métodos (method dispatch), que é responsável por identificar a classe do objeto e utilizar (“despachar”) a função método correta para aquela classe. Toda função genérica possui a mesma forma: uma chamada para a função UseMethod(), que especifica o nome genérico e o objeto a ser despachado. Por exemplo, veja o código fonte da função mean()

mean
function (x, ...) 
UseMethod("mean")
<bytecode: 0x55832c9eb590>
<environment: namespace:base>

Agora veja o código fonte da função mean.default, que é o método específico para vetores numéricos

mean.default
function (x, trim = 0, na.rm = FALSE, ...) 
{
    if (!is.numeric(x) && !is.complex(x) && !is.logical(x)) {
        warning("argument is not numeric or logical: returning NA")
        return(NA_real_)
    }
    if (isTRUE(na.rm)) 
        x <- x[!is.na(x)]
    if (!is.numeric(trim) || length(trim) != 1L) 
        stop("'trim' must be numeric of length one")
    n <- length(x)
    if (trim > 0 && n) {
        if (is.complex(x)) 
            stop("trimmed means are not defined for complex data")
        if (anyNA(x)) 
            return(NA_real_)
        if (trim >= 0.5) 
            return(stats::median(x, na.rm = FALSE))
        lo <- floor(n * trim) + 1
        hi <- n + 1 - lo
        x <- sort.int(x, partial = unique(c(lo, hi)))[lo:hi]
    }
    .Internal(mean(x))
}
<bytecode: 0x55832cdb9058>
<environment: namespace:base>

Agora suponha que você ddeseja criar uma função que calcule a média para um objeto de uma classe diferente daquelas previamente definidas. Por exemplo, suponha que você quer que a função mean() retorne a média das linhas de uma matriz.

set.seed(1)
mat <- matrix(rnorm(50), nrow = 5)
mean(mat)
[1] 0.1004483

O resultado é a média de todos os elementos, e não de cada linha. Nesse caso, podemos definir nossa própria função método para fazer o cálculo que precisamos. Por exemplo:

mean.matrix <- function(x, ...) rowMeans(x)

Uma função método é sempre definida dessa forma: <funçãogenérica>.<classe>. Agora podemos ver novamente os métodos disponíveis para a função mean()

methods(mean)
[1] mean.Date        mean.default     mean.difftime    mean.matrix     
[5] mean.POSIXct     mean.POSIXlt     mean.quosure*    mean.vctrs_vctr*
see '?methods' for accessing help and source code

e simplesmente aplicar a função genérica mean() à um objeto da classe matrix para obter o resultado que desejamos

class(mat)
[1] "matrix" "array" 
mean(mat)
[1]  0.09544402  0.12852087  0.06229588 -0.01993810  0.23591872

Esse exemplo ilustra como é simples criar funções método para diferentes classes de objetos. Poderíamos fazer o mesmo para objetos das classes data.frame e list

mean.data.frame <- function(x, ...) sapply(x, mean, ...)
mean.list <- function(x, ...) lapply(x, mean)

Aplicando em objetos dessas classes específicas, obtemos:

## Data frame
set.seed(1)
da <- data.frame(c1 = rnorm(10),
                 c2 = runif(10))
class(da)
[1] "data.frame"
mean(da)
       c1        c2 
0.1322028 0.4183230 
## Lista
set.seed(1)
dl <- list(rnorm(10), runif(50))
class(dl)
[1] "list"
mean(dl)
[[1]]
[1] 0.1322028

[[2]]
[1] 0.4946632

Obviamente esse processo todo é extremamente importante ao se criar novas funções no R. Podemos tanto criar uma função genérica (como a mean()) e diversos métodos para ela usando classes de objetos existentes, quanto (inclusive) criar novas classes e funções método para elas. Essa é uma das grandes lberdades que o método S3 de orientação à objetos permite, e possivelmente um dos motivos pelos quais é relativamente simples criar pacotes inteiros no R.