5

Similar to the post on this topic for S4 classes, I am wondering about the resolution method and the conflict prevention for S3 classes.

Let me show an example. I created two packages: pkga and pkgb. Both happen to have an S3 class constructor function returning an object whose class has the same name myclass

#' @export
myclass_instance <- function() {
  structure(list(name = "pkga"), class = c("myclass"))
}


#' @export
whatever <- function(obj) {
  UseMethod("whatever", obj)
}

#' @export
whatever.default <- function(obj) {
  print("pkga::whatever.default")
}

#' @export
whatever.myclass <- function(obj) {
  print("pkga::whatever.myclass")
}

and for pkgb

#' @export
myclass_instance <- function() {
  structure(list(name = "pkgb"), class = c("myclass"))
}


#' @export
whatever <- function(obj) {
  UseMethod("whatever", obj)
}


#' @export
whatever.default <- function(obj) {
  print("pkgb::whatever.default")
}

#' @export
whatever.myclass <- function(obj) {
  print("pkgb::whatever.myclass")
}

Now, if I have both packages installed, and try the following:

> aobj<-pkga::myclass_instance()
> pkga::whatever(aobj)
[1] "pkga::whatever.myclass"
> pkga::whatever(3)
[1] "pkga::whatever.default"

This is all correct. However, if I create an object from pkgb, and then pass it to pkga::whatever it will not apply the default as I would expect (they are two completely unrelated classes that happen to have the same name), because the resolution is only made on the name

> bobj<-pkgb::myclass_instance()
> pkga::whatever(bobj)
[1] "pkga::whatever.myclass"

This can give strange effects if two unrelated libraries happen to have the same class name, and the generic name happen to conflict. While a rare and unlikely occurrence, from the formal point of view I don't like it one bit, so I was wondering if there's a way to prevent this from happening that is better than prefixing the class name with the package name, e.g.

#' @export
myclass_instance <- function() {
  structure(list(name = "pkga"), class = c("pkga::myclass"))
}


#' @export
whatever <- function(obj) {
  UseMethod("whatever", obj)
}

#' @export
whatever.default <- function(obj) {
  print("pkga::whatever.default")
}

#' @export
`whatever.pkga::myclass` <- function(obj) {
  print("pkga::whatever.myclass")
}

and similar for pkgb.

Stefano Borini
  • 138,652
  • 96
  • 297
  • 431
  • 2
    I assume it's the standard function masking where the function from whichever package was loaded last will be used when there are name conflicts. And using `::` is the standard way to be safe about the risk of conflicts. There's nothing special about whether the class defined is in base, pkga, pkgb, or any other package. You can look at `conflicts()` to see what functions are masked, and the `conflicted` package might be of interest too. – Gregor Thomas May 19 '22 at 16:29
  • 3
    *"from the formal point of view I don't like it one bit"*. S3 is notoriously informal. It's stupid simple---all it is is function dispatch based on the name of the class of the first argument. Like most very simple systems, the simplicity is both a benefit and a drawback. – Gregor Thomas May 19 '22 at 16:33
  • @GregorThomas this is not about importing the functions in the same namespace (e.g. using library()). This is about dispatching to the appropriate method even when qualified, when the dispatch is determined, it appears, exclusively through the name of the class, regardless of where that class was actually defined. – Stefano Borini May 19 '22 at 16:40
  • 2
    Ah, I missed that you were deliberately calling `pkg::whatever` on `bobj`. I suppose you could write a function that attempts to check if your object was actually created by package a (I'm not sure exactly what that check would be....) and apply the check in your non-default methods with an escape hatch to the `.default` version if the check fails. But that sounds like more work than change the name of the class to make it more unique as you suggest, or using a more robust class system that already implements something along those lines. – Gregor Thomas May 19 '22 at 16:50
  • as stated `::` is explicit on the 'who is your daddy' from L->R,. `?` (I think) only offers disambiguation of loaded library name conflicts, but I'm nearly certain I've seem some sort of NLP/regex extension of this to unattached, usr site-library functions, and am otherwise unqualified to comment, but were the universe of same name knowable, would result in a warn. – Chris May 19 '22 at 19:37
  • 1
    What do you have control over in this scenario? Presumably you have control over the creation of one of the S3 classes (which could clash with a random package), and the specific S3 methods for that class (but not necessarily the generic function, i.e. you want something that will work with `print` and `summary` and `plot`) – Allan Cameron May 27 '22 at 11:46
  • There is no way for a generic function to determine how the class attribute has been set on an object. So you cannot instruct an S3 method to do something different with respect on who assigned the class on the input object. The S3 class is an attribute and not a property of an object. – nicola May 31 '22 at 09:58
  • 1
    "I am wondering about ... the conflict prevention for S3 classes." There is no conflict prevention. "I was wondering if there's a way to prevent this from happening that is better" No, be creative with your class names. – Roland May 31 '22 at 12:05
  • 1
    Is there really a problem here? If you have two unrelated packages that use the same generic name and class name each will favor the methods in its own package. If they are unrelated why would you want the generic of one package to invoke a method from the other? Also note that normally the methods do not have to be directly accessed so they would not be exported from the package so that only that package's generic could know about them. – G. Grothendieck Jun 01 '22 at 18:09

1 Answers1

0

Packages a and b use different namespaces. S3 is built to be informal, so the only way around this is to use package name prefixing (i. e. pkg::whatever).

Fixing this would require a level of specificity that is lacking in R (apart from explicit prefixing). According to this chapter in R Packages, this is a known phenomenon.

luke
  • 465
  • 1
  • 14