Skip to content

[R-pkg-devel] Is a double dispatch possible with two S3 classes?

5 messages · Leonardo Silvestri, Duncan Murdoch, Jens Oehlschlägel

#
Hello,

I have stumbled upon an S3 class dispatch issue which is easily solved 
using S4 classes, but I'd like to know if there is a way to solve it 
without changing the type of class used.

The problem is as follows. There is an S3 class 'foo' which is defined 
in a package over which I don't have control. Then there is an S3 class 
'bar' derived from 'foo' in a package that can be modified.

Here is some code for 'foo':

as.foo <- function(x) {
   oldClass(x) <- "foo"
   x
}

print.foo <- function(x, ...) print(paste(x, "foo"))

"-.foo" <- function(e1, e2) "-.foo was called"


And here is some code for 'bar':

as.bar <- function(x) {
   oldClass(x) <- c("bar", "foo")
   x
}

print.bar <- function(x, ...) print(paste(x, "bar"))


Now the '-' operator must be defined in such a way that the behaviour is 
different depending on the operand classes, and in particular 
'bar'-'bar' needs to be different from 'bar'-'foo'. If I define the 
following function:

"-.bar" <- function(e1, e2) "-.bar was called"

then I get the following results.

as.foo(1) - as.foo(2)     # uses '-.foo'
as.bar(1) - as.bar(2)     # uses '-.bar'
as.bar(1) - as.foo(2)     # uses '-.default' and issues
                           # Incompatible methods warning
as.foo(1) - as.bar(2)     # uses '-.default' and issues
                           # Incompatible methods warning

So that seems like a dead end.

If, overcoming an instinctive shudder of disgust, I redefine '-.foo' in 
the "bar" package, I have checked that correctness is at the mercy of 
the order of loading of the packages, even though the "bar" package 
imports the "foo" package. So that doesn't seem to work either.

Before committing to making 'bar' an S4 class, does anyone know if there 
is another option that would allow 'bar' to remain an S3 class?

Thanks,
Leo
#
On 16/02/2017 11:52 AM, Leonardo Silvestri wrote:
I don't think so, though you could conceivably check whether things will 
work when bar is loaded, and fail with a message saying "bar must be 
loaded first!" or whatever.

Duncan Murdoch
1 day later
#
On 02/16/2017 02:34 PM, Duncan Murdoch wrote:
Thanks for the answer. Yes, that would be a possibility (using 'foo' in 
'Depends' rather than 'Imports' does guarantee the loading order). One 
downside though is that two packages can't play at that game 
simultaneously...

Leo
4 days later
#
Leonardo,

I had a similar problem when I tried to implement reasonable behaviour for logical operators for classes .bit and .bitwhich in package 'bit'.

Part of your problem is that R refuses to dispatch to any of two incompatible classes and instead falls back to a meaningless default. 

In theory, *this* problem *should* be solved in S3 by having a common super-class 'foobar' for class(foo) <- c('foo','foobar') and  class(bar) <- c('bar','foobar'), but again, R does refuses to dispatch on the common super-class (it directly tries to dispatch on the most special subclasses it finds and does not check for a common class in the class hierarchy). 

However, it appears that by having a common sub-class 'foobar' for class(foo) <- c('foobar','foo') and  class(bar) <- c('foobar','bar') we can get a common dispatch and hence get control. I plan for my next release of package 'bit' a common subclass 'booltype' to which R can dispatch without conflict. If you can convince the author of 'foo' to use class(foo) <- c('foobar','foo') and base his dispatch on 'foo', then you can dispatch on 'foobar' with no conflict and use NextMethod to invoke his code for the foo-foo case, and your code for the other cases. If the author refuses for some reason, you can still provide a wrapper to convert class 'foo' into class c('foobar','foo'), or do more nasty things like patching his class generator, but at least you don't need to patch his methods! 

I admit, using a physical subclass as a logical superclass is ugly: instead of declaring 'apples' and 'oranges' as special cases of 'fruits', we tell R that 'a fruit is an apple' and that 'a fruit is an orange', such that R dispatches to fruit-code for any combination of apples and oranges. Well ... the good thing is that NextMethod can invoke the original apple-code or orange-code, and we only need to write apple-orange-code and orange-apple-code to handle the new combinations. 

@R-devel: any comments or suggestions?

HTH


Jens?Oehlschl?gel


P.S. Example code and output follows


# If we try to have 'foo' and 'bar' as specializations of of a superclass 'foobar'

f <- "fooobj"
oldClass(f) <- c('foo','foobar')
"+.foo" <- function(e1,e2){
  paste(e1,'+foo+',e2)
}

b <- "barobj"
oldClass(b) <- c('bar','foobar')
"+.bar" <- function(e1,e2){
  paste(e1,'+bar+',e2)
}

"+.foobar" <- function(e1,e2){
  if (inherits(e1,"foo") && inherits(e2,"bar"))
    paste(e1,'+foobar+',e2)
  else if (inherits(e1,"bar") && inherits(e2,"foo"))
    paste(e1,'+barfoo+',e2)
  else stop("should not be invoked for foo+foo or bar+bar (but it does)")
}

# Yes, this works
f + f
b + b
# but here R does not even dispatch on the common superclass 'foobar'
f + b
b + f


# However, it seems that we can trick R to do the right thing pretending that 'foobar' is a subclass of both, 'foo' and 'bar'

oldClass(f) <- c('foobar','foo')
"+.foo" <- function(e1,e2){
  paste(e1,'+foo+',e2)
}

b <- "barobj"
oldClass(b) <- c('foobar','bar')
"+.bar" <- function(e1,e2){
  paste(e1,'+bar+',e2)
}

"+.foobar" <- function(e1,e2){
  if (inherits(e1,"foo") && inherits(e2,"bar"))
    paste(e1,'+foobar+',e2)
  else if (inherits(e1,"bar") && inherits(e2,"foo"))
    paste(e1,'+barfoo+',e2)
  else NextMethod(e1,e2)  # make sure we dispatch to foo+foo and bar+bar
}

# This works
f + f
b + b
# but R does not even dispatch on the common ground 'foobar'
f + b
b + f
+   paste(e1,'+foo+',e2)
+ }
+   paste(e1,'+bar+',e2)
+ }
+   if (inherits(e1,"foo") && inherits(e2,"bar"))
+     paste(e1,'+foobar+',e2)
+   else if (inherits(e1,"bar") && inherits(e2,"foo"))
+     paste(e1,'+barfoo+',e2)
+   else stop("should not be invoked for foo+foo or bar+bar (but it does)")
+ }
[1] "fooobj +foo+ fooobj"
[1] "barobj +bar+ barobj"
Fehler in f + b : nicht-numerisches Argument f?r bin?ren Operator
Zus?tzlich: Warnmeldung:
Inkompatible Methoden ("+.foo", "+.bar") f?r "+"
Fehler in b + f : nicht-numerisches Argument f?r bin?ren Operator
Zus?tzlich: Warnmeldung:
Inkompatible Methoden ("+.bar", "+.foo") f?r "+"
+   paste(e1,'+foo+',e2)
+ }
+   paste(e1,'+bar+',e2)
+ }
+   if (inherits(e1,"foo") && inherits(e2,"bar"))
+     paste(e1,'+foobar+',e2)
+   else if (inherits(e1,"bar") && inherits(e2,"foo"))
+     paste(e1,'+barfoo+',e2)
+   else NextMethod(e1,e2)  # make sure we dispatch to foo+foo and bar+bar
+ }
[1] "fooobj +foo+ fooobj"
[1] "barobj +bar+ barobj"
[1] "fooobj +foobar+ barobj"
[1] "barobj +barfoo+ fooobj"

?

Gesendet:?Donnerstag, 16. Februar 2017 um 17:52 Uhr
Von:?"Leonardo Silvestri" <lsilvestri at ztsdb.org>
An:?r-package-devel at r-project.org
Betreff:?[R-pkg-devel] Is a double dispatch possible with two S3 classes?
Hello,

I have stumbled upon an S3 class dispatch issue which is easily solved
using S4 classes, but I'd like to know if there is a way to solve it
without changing the type of class used.

The problem is as follows. There is an S3 class 'foo' which is defined
in a package over which I don't have control. Then there is an S3 class
'bar' derived from 'foo' in a package that can be modified.

Here is some code for 'foo':

as.foo <- function(x) {
oldClass(x) <- "foo"
x
}

print.foo <- function(x, ...) print(paste(x, "foo"))

"-.foo" <- function(e1, e2) "-.foo was called"


And here is some code for 'bar':

as.bar <- function(x) {
oldClass(x) <- c("bar", "foo")
x
}

print.bar <- function(x, ...) print(paste(x, "bar"))


Now the '-' operator must be defined in such a way that the behaviour is
different depending on the operand classes, and in particular
'bar'-'bar' needs to be different from 'bar'-'foo'. If I define the
following function:

"-.bar" <- function(e1, e2) "-.bar was called"

then I get the following results.

as.foo(1) - as.foo(2) # uses '-.foo'
as.bar(1) - as.bar(2) # uses '-.bar'
as.bar(1) - as.foo(2) # uses '-.default' and issues
# Incompatible methods warning
as.foo(1) - as.bar(2) # uses '-.default' and issues
# Incompatible methods warning

So that seems like a dead end.

If, overcoming an instinctive shudder of disgust, I redefine '-.foo' in
the "bar" package, I have checked that correctness is at the mercy of
the order of loading of the packages, even though the "bar" package
imports the "foo" package. So that doesn't seem to work either.

Before committing to making 'bar' an S4 class, does anyone know if there
is another option that would allow 'bar' to remain an S3 class?

Thanks,
Leo

______________________________________________
R-package-devel at r-project.org mailing list
https://stat.ethz.ch/mailman/listinfo/r-package-devel
1 day later
#
Jens,

Thanks for this detailed alternative. It's neat and works as you 
describe, but it does fall a little short of what S4 dispatch can do. In 
particular if a second package besides 'bar', say 'baz', needs to define 
operations 'foo + baz' or 'baz' + 'foo' it can't define 'baz::+.foobar' 
without breaking 'bar::+.foobar'...

Leo
On 02/22/2017 01:44 PM, "Jens Oehlschl?gel" wrote: