1

I've created a custom S4 class, and the idea is that it represents a vector that's always sorted, so I don't want sort() to actually do anything to it. So I defined a stub version of sort() for my class:

MyClass <- methods::setClass("MyClass", slots=list(x="numeric"))
setMethod("sort", signature(x="MyClass"), function(x, ...){}) # Do nothing

Then, I want to calculate a quantile of my class. R's quantile() function internally calls sort(). However, the sort() used inside quantile() is not aware of my S4 method, because it dispatches using UseMethod() (the S3 dispatcher) and not standardGeneric(), the S4 dispatcher. This is demonstrated below:

options(error=traceback)
instance = MyClass()
quantile(instance, 0.5)

This returns a call stack like this:

5: order(x, na.last = na.last, decreasing = decreasing)
4: sort.default(x, partial = unique(c(lo, hi)))
3: sort(x, partial = unique(c(lo, hi)))
2: quantile.default(instance, 0.5)
1: quantile(instance, 0.5)

Since sort.default is being called, it is evident that my custom sort implementation isn't being used.

Is there a simple way to get R to use my S4 method here? I realise I can also define sort.MyClass (the S3 way), but if I do this, what is the point of having an S4 method at all? It seems like S4 is incompatible with core R methods which renders it fairly useless.

Hieu Nguyen
  • 492
  • 3
  • 8
Migwell
  • 18,631
  • 21
  • 91
  • 160

1 Answers1

0

Object instance is defined with a slot named x that is numeric. When you call quantile(instance, 0.5), R do not know that you want quantile to act on the slot instance@x.
Approach 1:

MyClass <- setClass("MyClass", slots = list(x = "numeric"))

setMethod(
    "quantile",
    signature(x = "MyClass"),
    function(x, ...) {
        callNextMethod(x@x, ...)
    }
)

# test drive
instance <- MyClass(x = c(0, 5, 2, 1, 3))
quantile(instance, 0.5)
sort(instance) # error
mean(instance) # error

# see that quantile is now using S4 dispatch
quantile
standardGeneric for "quantile" defined from package "stats"

function (x, ...) 
standardGeneric("quantile")
<environment: 0x000001fe1375fe08>
Methods may be defined for arguments: x
Use  showMethods(quantile)  for currently available ones.

# see method table for quantile
showMethods(quantile, includeDefs = TRUE)
Function: quantile (package stats)
x="ANY"
function (x, ...) 
UseMethod("quantile")


x="MyClass"
function (x, ...) 
{
    callNextMethod(x@x, ...)
}

With this approach, you can see that quantile is automatically converted to using S4 dispatch.

  1. The call quantile(instance, 0.5) is dispatch to quantile,MyClass-method
  2. Inside quantile,MyClass-method, the code callNextMethod(x@x, ...) will dispatch to quantile,ANY-method with content of slot x as argument. This argument is numeric.
  3. Inside quantile,ANY-method, the code will S3 dispatch the calling arguments to quantile.default.

However, This approach require you to specify a customized version of every functions to act on MyClass. Therefore sort(instance) and mean(instance) output error.

Approach 2: Make MyClass as a subclass of numeric. Then all functions that work on numeric will work on MyClass. Below, I add a customized initialize method to automatically sort its numeric argument. A sort,MyClass-method to do no sorting and only return MyClass as numeric for consistency.

MyClass <- setClass("MyClass", contains = "numeric")
setMethod("initialize",
    signature(.Object = "MyClass"),
    function (.Object, ...)
    {      
        callNextMethod(.Object, sort(..1)) # ..1 is first element of ... see ?dots
    }
)

setMethod(
    "sort",
    signature(x = "MyClass"),
    function(x, decreasing = FALSE, ...) {
        as(x, "numeric")
    }
)

# test drive
instance <- MyClass(c(0, 5, 2, 1, 3))
quantile(instance, 0.5)
quantile(instance)
mean(instance)
sd(instance)
plot(instance)

Note:

setMethod("sort", signature(x="MyClass"), function(x, ...){}) # return NULL
setMethod("sort", signature(x="MyClass"), function(x, ...) x) # return x unchange
Hieu Nguyen
  • 492
  • 3
  • 8