5

I defined two classes that can successfully add two of their own objects or a number and one of their own objects.

a <- structure(list(val = 1), class = 'customClass1')
b <- structure(list(val = 1), class = 'customClass2')
`+.customClass1` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass1')
  return(structure(list(val = val_res), class = 'customClass1'))
}
`+.customClass2` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass2')
  return(structure(list(val = val_res), class = 'customClass1'))
}
print.customClass1 <- function(x, ...){
  print(x$val)
}
print.customClass2 <- function(x, ...){
  print(x$val)
}
a + a
# [1] 2
a + 1
# [1] 2
b + b
# [1] 2
1 + b
# [1] 2

But obviously, it goes wrong when I try to add the two custom classes.

a + b
# Error in a + b : non-numeric argument to binary operator
# In addition: Warning message:
# Incompatible methods ("+.customClass1", "+.customClass2") for "+" 

I could define just one function for customClass1, but then that function would not work when I try to add two customClass2 objects. Is there any way to prioritize one function over the other?

R seems to do this naturally by prioritizing my functions over the base functions (e.g. of the type numeric or integer). When one of both arguments has the customClass type, R automatically redirects it to my function instead of the default function.

takje
  • 2,630
  • 28
  • 47
  • @JoshuaUlrich I was trying to get an insight in how R chooses one function for a certain class over another internally. When the two types are similar there is no problem. When one of both arguments is of a base type and the other isn't, it seems to work fine. However, when you do this with different non-base types, it is harder. After rethinking this issue, it is probably easier to make a superclass from which both customClass1 and 2 inherit properties. – takje Mar 29 '17 at 07:31
  • 1
    @takje That would indeed be the best solution. See my answer for an idea on how to do that. – Joris Meys Mar 29 '17 at 12:51

2 Answers2

3

How R chooses which method to dispatch is discussed in the Details section of ?base::Ops

The classes of both arguments are considered in dispatching any member of this group. For each argument its vector of classes is examined to see if there is a matching specific (preferred) or 'Ops' method. 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.

If customClass1 and customClass2 are related, you can use a virtual class to allow operations using the two different classes. For example, you can mix POSIXct and POSIXlt because they both inherit from POSIXt. This is documented in ?DateTimeClasses:

"POSIXct" is more convenient for including in data frames, and "POSIXlt" is closer to human-readable forms. A virtual class "POSIXt" exists from which both of the classes inherit: it is used to allow operations such as subtraction to mix the two

For example:

class(pct <- Sys.time())
# [1] "POSIXct" "POSIXt"
Sys.sleep(1)
class(plt <- as.POSIXlt(Sys.time()))
# [1] "POSIXlt" "POSIXt"
plt - pct
# Time difference of 1.001677 secs

If the classes aren't related in this way, there's some good information in the answers to Emulating multiple dispatch using S3 for “+” method - possible?.

Community
  • 1
  • 1
Joshua Ulrich
  • 173,410
  • 32
  • 338
  • 418
3

Joshua explained why your approach can never work smoothly when using S3 without constructing virtual superclasses and the likes. With S3 you'll have to manually manage the class assignments in every possible function you use. Forget to assign the super class once, and you're off for a bug hunt that can last a while.

I would strongly suggest to abandon S3 and move to S4. Then you can define the methods in both directions for the group "Ops". This has the advantage that all arithmetic, logic and comparison operators are now defined for both classes. If you want to limit this to a subgroup or a single operator, replace "Ops" by the subgroup or operator. More info on the help page ?S4GroupGeneric.

An example based on your S3 classes using a virtual class to make things easier:

# Define the superclass
setClass("super", representation(x = "numeric"))
# Define two custom classes
setClass("foo", representation(slot1 = "character"),
         contains = "super")
setClass("bar", representation(slot1 = "logical"),
         contains = "super")

# Set the methods
setMethod("Ops",
          signature = c('super','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('ANY','super'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })
# Redundant actually, but limits the amount of times callGeneric
# has to be executed. 
setMethod("Ops",
          signature = c('super','super'),
          function(e1,e2){
            callGeneric(e1@x, e2@x)
          })

foo1 <- new("foo", x = 3, slot1 = "3")
bar1 <- new("bar", x = 5, slot1 = TRUE)

foo1 + bar1
#> [1] 8
bar1 + foo1
#> [1] 8
bar1 < foo1
#> [1] FALSE
foo1 / bar1
#> [1] 0.6

An example with 2 classes where the slot names are different:

setClass("foo", representation(x = "numeric"))
setClass("bar", representation(val = "numeric"))

setMethod("Ops",
          signature = c('foo','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('bar','ANY'),
          function(e1,e2){
            callGeneric(e1@val, e2)
          })
setMethod("Ops",
          signature = c('ANY','bar'),
          function(e1,e2){
            callGeneric(e1, e2@val)
          })
setMethod("Ops",
          signature = c('ANY','foo'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })

Again you can use the code above to check the results. Note that here you will get a note about the chosen methods when trying this interactively. To avoid that, you can add a method for signature c('foo','bar') and c('bar','foo')

Joris Meys
  • 106,551
  • 31
  • 221
  • 263
  • This is a really good suggestion. So far I've only used S3. I will look into this and it will be a cleaner solution. However, Joshua's answer is closer to the question and therefore I've accepted his answer. – takje Mar 29 '17 at 14:03
  • 1
    @takje no worries, I just added this answer to illustrate how S4 tackles this problem in a more robust manner. If you like to give it a shot: Hadley has a great introduction to S4 in his book Advanced R: http://adv-r.had.co.nz/S4.html In his book on packages, he also explains how you can easily deal with the exporting and documentation of S4 classes and methods using roxygen2: http://r-pkgs.had.co.nz/man.html#man-classes and http://r-pkgs.had.co.nz/namespace.html#exports – Joris Meys Mar 29 '17 at 14:06