Adding S4 Dispatch to Base R S3 Generic

Adding S4 dispatch to base R S3 generic

The mis-dispatch occurs because the body of the generic is not "standard" (I think the rationale is that, since you've done something other than invoke standardGeneric("merge"), you know what you're doing so no automatic default; maybe I'm making this up and it's really a bug). Solutions are to set a standard generic allowing for the default dispatch

setGeneric("merge")

or to explicitly provide standard dispatch

setGeneric("merge", function(x, y, ...) standardGeneric("merge"))

or explicitly specify a default method

setGeneric("merge", function(x, y, ...){
cat("generic dispatch\n")
standardGeneric("merge")
}, useAsDefault=base::merge)

Combining S4 and S3 methods in a single function

The section "Methods for S3 Generic Functions" of ?Methods suggest an S3 generic, an S3-style method for S4 classes, and the S4 method itself.

setClass("A")                    # define a class

f3 <- function(x, ...) # S3 generic, for S3 dispatch
UseMethod("f3")
setGeneric("f3") # S4 generic, for S4 dispatch, default is S3 generic
f3.A <- function(x, ...) {} # S3 method for S4 class
setMethod("f3", "A", f3.A) # S4 method for S4 class

The S3 generic is needed to dispatch S3 classes.

The setGeneric() sets the f3 (i.e., the S3 generic) as the default, and f3,ANY-method is actually the S3 generic. Since 'ANY' is at (sort of) the root of the class hierarchy, any object (e.g., S3 objects) for which an S4 method does not exist ends up at the S3 generic.

The definition of an S3 generic for an S4 class is described on the help page ?Methods. I think, approximately, that S3 doesn't know about S4 methods, so if one invokes the S3 generic (e.g., because one is in a package name space where the package knows about the S3 f3 but not the S4 f3) the f3 generic would not find the S4 method. I'm only the messenger.

S3 generic/method consistency - How to dispatch according to chosen type?

In the code in the question MyFun is not a generic and MyFun.algo1 and MyFun.algo2 are not S3 methods even though the names seem to suggest that. It would be beter to change it to something like this which is not suggestive of something it is not, won't trigger any checks and is more compact.

Myfun <- function(x, type = c("algo1", "algo2"), A, B, ...) {
type <- match.arg(type)
do.call(type, list(x, A, B, ...))
}

algo1 <- function(x, A, B, ...) "algo1"
algo2 <- function(x, A, B, ...) "algo2"

# test run
Myfun(1, "algo1", 2, 3)
## [1] "algo1"

# another test run
Myfun(1, A = 2, B = 3)
## [1] "algo1"

emulating multiple dispatch using S3 for + method - possible?

You can do it by defining +.a and +.b as the same function. For example:

a <- "a"
class(a) <- "a"
b <- "b"
class(b) <- "b"

`+.a` <- function(e1, e2){
paste(class(e1), "+", class(e2))
}
`+.b` <- `+.a`

a+a
# [1] "a + a"
a+b
# [1] "a + b"
b+a
# [1] "b + a"
b+b
# [1] "b + b"

# Other operators won't work
a-a
# Error in a - a : non-numeric argument to binary operator

If you define Ops.a and Ops.b, it will also define the operation for other operators, which can be accessed by .Generic in the function:

##### Start a new R session so that previous stuff doesn't interfere ####
a <- "a"
class(a) <- "a"
b <- "b"
class(b) <- "b"

Ops.a <- function(e1, e2){
paste(class(e1), .Generic, class(e2))
}

Ops.b <- Ops.a

a+a
# [1] "a + a"
a+b
# [1] "a + b"
b+a
# [1] "b + a"
b+b
# [1] "b + b"

# Ops covers other operators besides +
a-a
# [1] "a - a"
a*b
# [1] "a * b"
b/b
# [1] "b / b"

Update: one more thing I discovered while playing with this. If you put this in a package, you'll get the "non-numeric argument" error and "incompatible operators" warning. This is because R is only OK with the multiple operators if they are exactly the same object, with the same address in memory -- but somehow in the building and loading of a package, the two functions lose this exact identity. (You can check this by using pryr::address())

One thing I've found that works is to explicitly register the S3 methods when the package is loaded. For example, this would go inside your package:

# Shows the classes of the two objects that are passed in
showclasses <- function(e1, e2) {
paste(class(e1), "+", class(e2))
}

.onLoad <- function(libname, pkgname) {
registerS3method("+", "a", showclasses)
registerS3method("+", "b", showclasses)
}

In this case, the two methods point to the exact same object in memory, and it works (though it's a bit of a hack).

How do you get S3 methods to work with S4 objects?

S4 is a superclass (virtual class, whatever, somebody please chime in with the correct name) that cannot be used directly dispatching. Same for S3 by the way. You can do S3-dispatching for S4 classes the way you do with S3 classes. On a sidenote, if nothing is specified then calling myfun on an S4 object will just lead to the .default function. :

myfun <- function(object, ...) UseMethod("myfun")

myfun.default <- function(object,...){
cat("default method.\n")
print(object)
}

myfun.gWindow <- function(object,...){
cat("Here here...")
print(object)
}

x <- 1:10
myfun(x)
myfun(win)
rm(myfun.gWindow)
myfun(win)

If you want to catch all S4 methods, you can dispatch manually in your .default function or generic function, using isS4(). Adding the dispatching in the .default function allows for automatic S3 dispatching to some S4 classes. If you add it in the generic, you just dispatch to all S4 no-matter-what :

    myfun.default <- function(object,...){
if(isS4(object)) myfun.S4(object,...)
else {
cat("default method.\n")
print(object)
}
}

myfun.S4 <- function(object,...){
cat("S4 method\n")
print(object)
}

x <- 1:10
myfun(x)
myfun(win)

Regarding your second question: gWindow is a special case. It also returns an error when tried with str(win). I don't know the exact structure, but it's definitely not a normal S4 object.

Multiple dispatch for `subset` methods in R

Normally in this situation you would set subset as an S4 generic but since you have reasons for not wanting to do that, you can get around this by defining a separate generic and calling it from within the S3 method, along the lines of

mySubset <- function(x,subset){
stop("this is only a generic function: it should never be called!")
}
setGeneric("mySubset")
## methods for mySubset
setMethod(
f = "mySubset",
signature = c(x = "myclass", subset = "logical"),
definition = function(x, subset){
# function body
}
)

setMethod(
f = "mySubset",
signature = c(x = "myclass", subset = "character"),
definition = function(x, subset){
# different function body
}
)

## default method using "ANY" (lower priority)
setMethod(
f = "mySubset",
signature = c(x = "myclass", subset = "ANY"),
definition = function(x, subset){
## insert default behaviour (might be an error),
## a call to subset.default or whatever
}
)

## now write an S3 method for subset that calls the S4 generic if
## x is of class myclass

subset.myClass <- function(x,subset){
mySubset(x,subset)
}

This preserves the S3-only behaviour of subset, but you now have S4 level control over method dispatch provided that x is of class myclass.

Your users don't need to appreciate this distinction; they can still call subset(x,class) in the same way they are accustomed to, when x has your new class.

r - S3 generic/method consistency when different arguments needed

I am not sure I would use S3 here. Not answering your specific question, but consider something like this. Of course, this is irrelevant if your actual use case is far enough away from your example.

foo <- function(x, y) {
if (is.list(x)) return(do.call(foo, x))

paste(x, y)
}

So now you can get the behavior you want, and just document x as being a list of arguments or the first argument. Otherwise, you'll create some annoying things such as requiring the user to explicitly name the argument every time.

foo("a", "b")
# [1] "a b"

foo(list(x = "a", y = "b"))
# [1] "a b"

foo(list(y = "a", x = "b"))
# [1] "b a"

foo("a", "b", z = "c")
# Error in foo("a", "b", z = "c") : unused argument (z = "c")

foo(list("a", "b", z = "c"))
# Error in (function (x, y) : unused argument (z = "c")

If compelled, you can accomplish the same thing using S3. But either way, you probably want to just name the argument the same thing and document it as having two meanings.

foo <- function(x, y, ...) UseMethod('foo')

foo.default <- function(x, y, ...) paste(x, y)

foo.list <- function(x, y, ...) do.call(foo, x)

An example of this is the plot function in base.

x
the coordinates of points in the plot. Alternatively, a single plotting structure, function or any R object with a plot method can be provided.

y
the y coordinates of points in the plot, optional if x is an appropriate structure.

Here, x is either the x coordinate or a valid plotting structure. And then y is only used if needed.



Related Topics



Leave a reply



Submit