Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Iñaki Ucar
CCing r-devel.

On Tue, 14 May 2019 at 02:11, Pavel Krivitsky <[hidden email]> wrote:

>
> Dear All,
>
> I've run into this while updating a package with unfortunately named
> legacy functions. It seems like something that might be worth changing
> in R, and I want to get a sense of whether this is a problem before
> submitting a report to the Bugzilla.
>
> It appears that the 3-argument form of S3method() in NAMESPACE controls
> dispatching when the generic is called from outside the package that
> defines it but not when it's called from inside the package that
> defines it.
>
> For example the attached minimal package has four R functions:
>
>    gen <- function(object, ...)
>      UseMethod("gen")
>
>    .gen.formula <- function(object, ...){
>      message("I am the S3method-declared method.")
>    }
>
>    gen.formula <- function(object, ...){
>      message("I am the function with an unfortunate name.")
>    }
>
>    test_me <- function(){
>      message("I am the tester. Which one will I call?")
>      gen(a~b)
>    }
>
> and the following NAMESPACE:
>
>    export(gen)
>    S3method(gen, formula, .gen.formula)
>    export(gen.formula)
>    export(test_me)
>
> Now,
>
>    library(anRpackage)
>    example(test_me)
>
> results in the following:
>
>    test_m> test_me
>    function(){
>      message("I am the tester. Which one will I call?")
>      gen(a~b)
>    }
>    <bytecode: 0x562fb9d32d68>
>    <environment: namespace:anRpackage>
>
>    test_m> test_me() # Calls gen.formula()
>    I am the tester. Which one will I call?
>    I am the function with an unfortunate name.
>
>    test_m> gen(a~b) # Calls .gen.formula()
>    I am the S3method-declared method.
>
> So, calling the same generic function with the same class results in
> different dispatching behaviour depending on whether the call is from
> within the package doing the export or from the outside.

It does not depend on whether you export gen.formula() or not. When
you call gen() inside your package, the S3 dispatch mechanism finds a
method gen.formula defined in that environment (the package's
namespace), so it is called.

> This behaviour appears to be as documented (
> https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Method-dispatching
> ), but it seems to me that if S3method() is allowed to give the name of
> the method to be used, then it should should override the name-based
> dispatching both inside and outside the package.
>
> Any thoughts?

Note that disabling name-based dispatch implies two things: 1) the
inability to override your method by defining gen.formula in the
global environment, and 2) another package can break yours (i.e.,
internal calls to gen()) by registering an S3 method for gen() after
you. I don't think that's a good idea.

Iñaki

______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Pavel Krivitsky
Hi, Iñaki,

Thanks for looking into this.

On Tue, 2019-05-14 at 11:33 +0200, Iñaki Ucar wrote:
> > So, calling the same generic function with the same class results
> > in different dispatching behaviour depending on whether the call is
> > from within the package doing the export or from the outside.
>
> It does not depend on whether you export gen.formula() or not. When
> you call gen() inside your package, the S3 dispatch mechanism finds a
> method gen.formula defined in that environment (the package's
> namespace), so it is called.

Indeed, this is the case. The issue is that when both are available and
exported, S3method takes precedence outside the package but function
name takes precedence inside the package.

> Note that disabling name-based dispatch implies two things: 1) the
> inability to override your method by defining gen.formula in the
> global environment, and 2) another package can break yours (i.e.,
> internal calls to gen()) by registering an S3 method for gen() after
> you.

That's a good point.

   > library(anRpackage)
   > gen(a~b)
   I am the S3method-declared method.
   > gen.formula <- function(object, ...){message("I am the externally declared method.")}
   > gen(a~b)
   I am the externally declared method.
   > test_me()
   I am the tester. Which one will I call?
   I am the function with an unfortunate name.

In that case, I think that the least surprising behaviour would
prioritise declarations and methods "nearer" to the caller over those
"farther" from the caller (where "caller" is the caller of the generic,
not the generic itself), and, within that, give precedence to S3method
declarations over function names.

That is, for a call from inside a package, the order of precedence
would be as follows:
   1. S3method() in that package's NAMESPACE.
   2. Appropriately named function in that package (exported or not).
   3. Appropriately named function in calling environment (which may be
      GlobalEnv).
   4. S3method() in other loaded packages' NAMESPACEs.
   5. Appropriately named functions exported by other loaded packages'
      NAMESPACEs.

For a call from outside a package, the precedence is the same, but 1
and 2 are not relevant.

As far as I can tell, this is the current behaviour except for the
relative ordering of 1 and 2.

                                Best,
                                Pavel

--
Pavel Krivitsky
Lecturer in Statistics
National Institute of Applied Statistics Research Australia (NIASRA)
School of Mathematics and Applied Statistics | Building 39C Room 154
University of Wollongong NSW 2522 Australia
T +61 2 4221 3713
Web (NIASRA): http://niasra.uow.edu.au/index.html
Web (Personal): http://www.krivitsky.net/research
ORCID: 0000-0002-9101-3362

NOTICE: This email is intended for the addressee named and may contain
confidential information. If you are not the intended recipient, please
delete it and notify the sender. Please consider the environment before
printing this email.

______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel

anRpackage_1.0.tar.gz (1K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Iñaki Ucar
On Tue, 14 May 2019 at 12:31, Pavel Krivitsky <[hidden email]> wrote:

>
> > Note that disabling name-based dispatch implies two things: 1) the
> > inability to override your method by defining gen.formula in the
> > global environment, and 2) another package can break yours (i.e.,
> > internal calls to gen()) by registering an S3 method for gen() after
> > you.
>
> That's a good point.
>
>    > library(anRpackage)
>    > gen(a~b)
>    I am the S3method-declared method.
>    > gen.formula <- function(object, ...){message("I am the externally declared method.")}
>    > gen(a~b)
>    I am the externally declared method.
>    > test_me()
>    I am the tester. Which one will I call?
>    I am the function with an unfortunate name.
>
> In that case, I think that the least surprising behaviour would
> prioritise declarations and methods "nearer" to the caller over those
> "farther" from the caller (where "caller" is the caller of the generic,
> not the generic itself), and, within that, give precedence to S3method
> declarations over function names.

The thing is that, in R, "nearer" means "the calling environment" (and
then, other things). When you call test_me(), the calling environment
for gen() is the package namespace. When you call gen() directly, then
the calling environment is the global environment. So what happens
here follows the principle of least astonishment.

The issue here is that you are registering a non-standard name
(.gen.formula) for that generic and then defining what would be the
standard name (gen.formula) for... what purpose? IMHO, this is a bad
practice and should be avoided.

> That is, for a call from inside a package, the order of precedence
> would be as follows:
>    1. S3method() in that package's NAMESPACE.
>    2. Appropriately named function in that package (exported or not).
>    3. Appropriately named function in calling environment (which may be
>       GlobalEnv).
>    4. S3method() in other loaded packages' NAMESPACEs.
>    5. Appropriately named functions exported by other loaded packages'
>       NAMESPACEs.
>
> For a call from outside a package, the precedence is the same, but 1
> and 2 are not relevant.
>
> As far as I can tell, this is the current behaviour except for the
> relative ordering of 1 and 2.

Nope. Current behaviour (see details in ?UseMethod) is:

"To support this, UseMethod and NextMethod search for methods in two
places: in the environment in which the generic function is called,
and in the registration data base for the environment in which the
generic is defined".

Changing this would probably break a lot of things out there.

Iñaki

______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Pavel Krivitsky
Hi, Inaki,

On Tue, 2019-05-14 at 12:50 +0200, Iñaki Ucar wrote:
> The thing is that, in R, "nearer" means "the calling environment"
> (and then, other things). When you call test_me(), the calling
> environment for gen() is the package namespace. When you call gen()
> directly, thenthe calling environment is the global environment. So
> what happens here follows the principle of least astonishment.

I don't think that we disagree about which environment takes
precedence. The issue is whether *within a a given environment*,
registration or function naming should take precedence.

> The issue here is that you are registering a non-standard name
> (.gen.formula) for that generic and then defining what would be the
> standard name (gen.formula) for... what purpose? IMHO, this is a bad
> practice and should be avoided.

The situation initially arose when I wanted to soft-deprecate calling a
particular method by its full name in order to clean up the package's
namespace.

To use our working example, I wanted calls to gen.formula() to issue a
deprecation warning, but calls to gen(formula) not to. The simplest way
to do that that I could find was to create a function, say,
.gen.formula() that would implement the method and declare it as the S3
export, and modify gen.formula() to issue the warning before passing on
to .gen.formula(). Then, direct calls to gen.formula() would produce a
warning, but gen(formula) would by pass it.

> > That is, for a call from inside a package, the order of precedence
> > would be as follows:
> >    1. S3method() in that package's NAMESPACE.
> >    2. Appropriately named function in that package (exported or
> > not).
> >    3. Appropriately named function in calling environment (which
> > may be
> >       GlobalEnv).
> >    4. S3method() in other loaded packages' NAMESPACEs.
> >    5. Appropriately named functions exported by other loaded
> > packages'
> >       NAMESPACEs.
> >
> > For a call from outside a package, the precedence is the same, but
> > 1 and 2 are not relevant.
> >
> > As far as I can tell, this is the current behaviour except for the
> > relative ordering of 1 and 2.
>
> Nope. Current behaviour (see details in ?UseMethod) is:
>
> "To support this, UseMethod and NextMethod search for methods in two
> places: in the environment in which the generic function is called,
> and in the registration data base for the environment in which the
> generic is defined".

Can you be more specific where the sequence above contradicts the
current implementation (except for swapping 1 and 2)? As far as I can
tell, it's just a more concrete description of what's in the
documentation.

                        Best Regards,
                        Pavel

--
Pavel Krivitsky
Lecturer in Statistics
National Institute of Applied Statistics Research Australia (NIASRA)
School of Mathematics and Applied Statistics | Building 39C Room 154
University of Wollongong NSW 2522 Australia
T +61 2 4221 3713
Web (NIASRA): http://niasra.uow.edu.au/index.html
Web (Personal): http://www.krivitsky.net/research
ORCID: 0000-0002-9101-3362

NOTICE: This email is intended for the addressee named and may contain
confidential information. If you are not the intended recipient, please
delete it and notify the sender. Please consider the environment before
printing this email.
______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Iñaki Ucar
On Sat, 18 May 2019 at 23:34, Pavel Krivitsky <[hidden email]> wrote:

>
> > The issue here is that you are registering a non-standard name
> > (.gen.formula) for that generic and then defining what would be the
> > standard name (gen.formula) for... what purpose? IMHO, this is a bad
> > practice and should be avoided.
>
> The situation initially arose when I wanted to soft-deprecate calling a
> particular method by its full name in order to clean up the package's
> namespace.
>
> To use our working example, I wanted calls to gen.formula() to issue a
> deprecation warning, but calls to gen(formula) not to. The simplest way
> to do that that I could find was to create a function, say,
> .gen.formula() that would implement the method and declare it as the S3
> export, and modify gen.formula() to issue the warning before passing on
> to .gen.formula(). Then, direct calls to gen.formula() would produce a
> warning, but gen(formula) would by pass it.

IMO the simplest way to do this is to check who the caller was:

foo <- function(x) UseMethod("foo")
foo.bar <- function(x) {
  sc <- sys.call(-1)
  if (is.null(sc) || sc[[1]] != "foo")
    .Deprecated(msg="Calling 'foo.bar' directly is deprecated")
}

x <- 1
class(x) <- "bar"

foo(x)      # silent
foo.bar(x)  # a warning is issued

> > > That is, for a call from inside a package, the order of precedence
> > > would be as follows:
> > >    1. S3method() in that package's NAMESPACE.
> > >    2. Appropriately named function in that package (exported or
> > > not).
> > >    3. Appropriately named function in calling environment (which
> > > may be
> > >       GlobalEnv).
> > >    4. S3method() in other loaded packages' NAMESPACEs.
> > >    5. Appropriately named functions exported by other loaded
> > > packages'
> > >       NAMESPACEs.
> > >
> > > For a call from outside a package, the precedence is the same, but
> > > 1 and 2 are not relevant.
> > >
> > > As far as I can tell, this is the current behaviour except for the
> > > relative ordering of 1 and 2.
> >
> > Nope. Current behaviour (see details in ?UseMethod) is:
> >
> > "To support this, UseMethod and NextMethod search for methods in two
> > places: in the environment in which the generic function is called,
> > and in the registration data base for the environment in which the
> > generic is defined".
>
> Can you be more specific where the sequence above contradicts the
> current implementation (except for swapping 1 and 2)? As far as I can
> tell, it's just a more concrete description of what's in the
> documentation.

The description in the documentation means that point 3) in your list
goes always first, which automatically implies 2) if the generic is
defined in the same package.

Iñaki

______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Pavel Krivitsky
Hi, Inaki,

On Sun, 2019-05-19 at 16:59 +0200, Iñaki Ucar wrote:

> IMO the simplest way to do this is to check who the caller was:
>
> foo <- function(x) UseMethod("foo")
> foo.bar <- function(x) {
>   sc <- sys.call(-1)
>   if (is.null(sc) || sc[[1]] != "foo")
>     .Deprecated(msg="Calling 'foo.bar' directly is deprecated")
> }
>
> x <- 1
> class(x) <- "bar"
>
> foo(x)      # silent
> foo.bar(x)  # a warning is issued

f <- getS3method("foo","bar")
f(x) # spurious warning

foo.baz <- function(x) NextMethod("foo")
class(x) <- c("baz","bar")
foo(x) # spurious warning

Believe me, I spent a lot of time trying to get this to work, and I
tried even more sophisticated call stack alchemy, but people kept
getting false positives and negatives. (Take a look at my attempt in
the statnet.common package.)

> The description in the documentation means that point 3) in your list
> goes always first, which automatically implies 2) if the generic is
> defined in the same package.

Are you sure which package defines the generic matters? I've just ran
some tests with two packages and moving the generic around doesn't seem
to affect things: the calling function determines whose method is used.

It seems to me like there is no contradiction after all, except that I
propose that the registered method should take precedence within a
namespace.

The only situation in which it would change R's behaviour would be when
a package/namespace contains a function foo.bar() AND a NAMESPACE
containing S3method(foo,bar,not.foo.bar) AND calls foo() on objects of
type bar from inside the package. It is extremely unlikely to break any
existing code.

                                Best,
                                Pavel

--
Pavel Krivitsky
Lecturer in Statistics
National Institute of Applied Statistics Research Australia (NIASRA)
School of Mathematics and Applied Statistics | Building 39C Room 154
University of Wollongong NSW 2522 Australia
T +61 2 4221 3713
Web (NIASRA): http://niasra.uow.edu.au/index.html
Web (Personal): http://www.krivitsky.net/research
ORCID: 0000-0002-9101-3362

NOTICE: This email is intended for the addressee named and may contain
confidential information. If you are not the intended recipient, please
delete it and notify the sender. Please consider the environment before
printing this email.
______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel
Reply | Threaded
Open this post in threaded view
|

Re: [R-pkg-devel] Three-argument S3method declaration does not seem to affect dispatching from inside the package.

Iñaki Ucar
On Sun, 19 May 2019 at 23:23, Pavel Krivitsky <[hidden email]> wrote:

>
> Hi, Inaki,
>
> On Sun, 2019-05-19 at 16:59 +0200, Iñaki Ucar wrote:
> > IMO the simplest way to do this is to check who the caller was:
> >
> > foo <- function(x) UseMethod("foo")
> > foo.bar <- function(x) {
> >   sc <- sys.call(-1)
> >   if (is.null(sc) || sc[[1]] != "foo")
> >     .Deprecated(msg="Calling 'foo.bar' directly is deprecated")
> > }
> >
> > x <- 1
> > class(x) <- "bar"
> >
> > foo(x)      # silent
> > foo.bar(x)  # a warning is issued
>
> f <- getS3method("foo","bar")
> f(x) # spurious warning
>
> foo.baz <- function(x) NextMethod("foo")
> class(x) <- c("baz","bar")
> foo(x) # spurious warning

Checking the enclosing environment and whether was called through
NextMethod respectively covers these cases too.

> > The description in the documentation means that point 3) in your list
> > goes always first, which automatically implies 2) if the generic is
> > defined in the same package.
>
> Are you sure which package defines the generic matters? I've just ran
> some tests with two packages and moving the generic around doesn't seem
> to affect things: the calling function determines whose method is used.

If package A defines generic foo and package B defines method foo.bar
without registering nor exporting it, then foo can't find foo.bar.

> It seems to me like there is no contradiction after all, except that I
> propose that the registered method should take precedence within a
> namespace.
>
> The only situation in which it would change R's behaviour would be when
> a package/namespace contains a function foo.bar() AND a NAMESPACE
> containing S3method(foo,bar,not.foo.bar) AND calls foo() on objects of
> type bar from inside the package. It is extremely unlikely to break any
> existing code.

To try to avoid changing current behaviour if foo.bar is found, R
would need to check whether the enclosing environment is identical to
the enclosing environment of the registered method, and in that case,
give precedence to the latter (which, BTW, is exactly what you need to
do to fix the first spurious warning above).

And still, funny things may happen. For example, pkgA defines generic
foo, exports foo.bar and registers other.foo.bar instead of foo.bar.
Following your proposal, if I load pkgA and call foo for an object of
class bar, other.foo.bar is called. Then I load pkgB, which registers
just another method for foo.bar, and call foo again. What happens is
that the registered method belongs now to pkgB, which is a different
namespace, so we got different precedence, and foo.bar is called
instead.

Exceptions leads us to inconsistencies like this. I can't speak for R
core, but I don't think that the use case is compelling enough to take
that path.

Iñaki

______________________________________________
[hidden email] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel