Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

emulating multiple dispatch using S3 for "+" method - possible?

Tags:

r

r-s3

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?

like image 756
Mark Heckmann Avatar asked Dec 01 '12 12:12

Mark Heckmann


2 Answers

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).

like image 142
wch Avatar answered Sep 22 '22 15:09

wch


Well you cannot use that strategy. It is specifically prohibited as you discovered and documented as so in the help(Ops) page.

"If a method is found for just one argument or the same method is found for both, it is used. If different methods are found, there is a warning about ‘incompatible methods’: in that case or if no method is found for either argument the internal method is used."

So you would need to put all cases into the same method. (Tested and does succeed.)

like image 20
IRTFM Avatar answered Sep 23 '22 15:09

IRTFM