Search code examples
rr-s3

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


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?


Solution

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