17.1. S3 Classes

S3 system builds up on two concepts:

  • A class attribute attached to an object
  • Class specific implementation for generic methods

In the following, we provide an implementation of the generic method print for an object of class mylist:

print.mylist <- function(lst, ...){
  for (name in names(lst)){
      cat(name); cat(': '); cat(lst[[name]]); cat(' ')
  }
  cat('\n')
}

Now let us create a list object:

> l <- list(a=1, b=2, c=1:4)

Standard printing of list objects:

> l
$a
[1] 1

$b
[1] 2

$c
[1] 1 2 3 4

Let us change the class of l now:

> class(l) <- 'mylist'

Print l will now pick the new implementation:

> print(l)
a: 1 b: 2 c: 1 2 3 4
> l
a: 1 b: 2 c: 1 2 3 4

17.1.1. A Modified Gram Schmidt Algorithm Implementation

The function below implements the Modified Gram Schmidt. While the algorithm implementation is straightforward and not important for the discussion in this section, look at the returned object. The function is returning a list object whose class has been set to mgs:

mgs <- function(X){

  # Ensure that X is a matrix
  X <- as.matrix(X)
  # Number of rows
  m <- nrow(X)
  # Number of columns
  n <- ncol(X)
  if (m < n) {
    stop('Wide matrices are not supported.')
  }
  # Construct the empty Q and R matrices
  Q <- matrix(0, m, n)
  R <- matrix(0, n, n)
  for (j in 1:n){
    # Pick up the j-th column of X
    v <- X[, j]
    # compute the projection of v on existing columns
    if (j > 1){
      for (i in 1:(j-1)){
        # pick up the i-th Q vector
        q <- Q[, i]
        #cat('q: '); print(q)
        # compute projection of v on qi
        projection <- as.vector(q %*% v)
        #cat('projection: '); print(projection)
        # Store the projection in R
        R[i, j] <- projection
        # subtract the projection from v
        v <- v - projection * q
      }
    }
    # cat('v: ') ; print(v)
    # Compute the norm of the remaining vector
    v.norm <- sqrt(sum(v^2))
    # cat('v-norm: '); print(v.norm)
    # Store the norm in R
    R[j,j] <- v.norm
    # Place the normalized vector in Q
    Q[, j] <- v / v.norm
  }
  # Prepare the result
  result <- list(Q=Q, R=R, call=match.call())
  # Set the class of the result
  class(result) <- 'mgs'
  result
}

We can now provide implementations of generic methods for the objects returned by this object. For example, let us implement print for objects of type mgs:

print.mgs <- function(mgs){
  cat("Call: \n")
  print(mgs$call)
  cat('Q: \n')
  print(mgs$Q)
  cat('R: \n')
  print(mgs$R)
}

Let us compute the QR decomposition of a matrix using this algorithm:

> A <- matrix(c(3, 2, -1, 2, -2, .5, -1, 4, -1), nrow=3)
> res <- mgs(A)

When we print the result object, print.mgs will be called:

> res
Call:
mgs(X = A)
Q:
           [,1]       [,2]      [,3]
[1,]  0.8017837  0.5901803 0.0939682
[2,]  0.5345225 -0.7785358 0.3288887
[3,] -0.2672612  0.2134695 0.9396820
R:
         [,1]      [,2]       [,3]
[1,] 3.741657 0.4008919  1.6035675
[2,] 0.000000 2.8441670 -3.9177929
[3,] 0.000000 0.0000000  0.2819046

We can implement other (non-generic) methods for this class too:

mgs.q <- function(mgs){
  mgs$Q
}

mgs.r <- function(mgs){
  mgs$R
}

mgs.x <- function(mgs){
  mgs$Q %*% mgs$R
}

Note that these methods don’t verify that the object being passed is of class mgs.