I have two classes (a
and b
) and I want to define the +
method for them.
I need different methods for the four possible combinations of the two classes, i.e.:
a + a method 1
a + b method 2
b + a method 3
b + b method 4
I know I could use S4 for multiple dispatch, but I want to know if there is a way to emulate this behaviour using S3. My approach was the following:
a <- "b"
class(a) <- "a"
b <- "e"
class(b) <- "b"
Ops.a <- function(e1, e2){
if (class(e1) == "a" &
class(e2) == "a")
print("a & a")
if (class(e1) == "a" &
class(e2) == "b")
print("a & b")
if (class(e1) == "b" &
class(e2) == "a")
print("b & a")
NULL
}
a + a
a + b
b + a
All this works fine, but of course the following is not defined.
b + b
Now to cover this case I added another method definition.
Ops.b <- function(e1, e2){
if (class(e1) == "b" &
class(e2) == "b")
print("b & b")
NULL
}
This will cause b + b
to work but now a + b
and b + a
methods are inconsistent and will cause and error.
> a + b
error in a + b : non-numeric argument for binary operator
additional: warning:
incompatible methods ("Ops.a", "Ops.b") for "+"
Is there a way to define all four cases properly using S3?
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).