Suppose I'm calling function PackageFuncA which exists within a 3rd party package (i.e. a library from CRAN). PackageFuncA in turn calls PackageFuncB within the same 3rd party package. Is there a way to call PackageFuncA such that when it calls PackageFuncB, it will in fact call my own implimentation of PackageFuncB? In other words, can I intercept the call to PackageFuncB?
I think the solution involves creating my own PackageFuncB function and then calling PackageFuncA within the same environment and not the PackageFuncA's environment, but I couldn't get it to work with do.call nor eval.
Here is a one-liner that does it. Here PackageFuncA
is stats::acf
and PackageFuncB
is stats:::plot.acf
which we want to replace with my.plot.acf
. my.plot.acf
prints "Hello"
and then calls the real stats:::plot.acf
.
# we want this to run in place of stats:::plot.acf
my.plot.acf <- function(x, ...) { cat("Hello\n"); stats:::plot.acf(x, ...) }
# this does it
library(proto)
acf <- with(proto(environment(acf), acf = stats::acf, plot.acf = my.plot.acf), acf)
# test
acf(1:10)
A proto object is an environment such that any function inserted into the object via the proto
function has its environment automatically reset to that object. The first arg of proto()
is the parent of the proto object.
In the example above, its been set up so that the acf
variable refers to the version of acf
that was inserted into the proto object (which is the same as the original except its environment has been modified to be the proto object). When the new acf
function is run plot.acf
is a free variable (i.e. not defined in acf
) so it is looked up in acf
's parent and that is the environment in the proto object where it finds the new plot.acf
. acf
might have other free variables but in those cases as they are not found in the proto object it looks into the parent of the proto object which is the original environment of the original acf
. In terms of diagrams we have this where <-
means left side is parent of right side:
environment(stats::acf) <- proto object <- revised acf
and the proto object contains both the plot.acf
and the revised acf
.
We have also set the environment of the new plot.acf
to the proto object. We may or may not have needed to do this. In many cases it won't matter. If it were important not to set the environment of the new plot.acf
then it would be done like this because proto never sets the environment of functions inserted using [[...]]
:
acf <- with(p <- proto(environment(acf), acf = stats::acf), acf)
p[["plot.acf"]] <- my.plot.acf
In this example, both approaches work.
It would be possible to do all this with plain environments at the expense of having to use several lines of code:
# create new environment whose parent is the original acf's parent
e <- new.env(parent = environment(stats::acf))
# the next statement is only need to overwrite any acf you already might have from
# trying other code. If you were sure there was no revised acf already defined
# then the next line could be omitted. Its a bit safer to include it.
acf <- stats::acf
# This sets the environment of the new acf. If there were no acf already here
# then it would copy it from stats::acf .
environment(acf) <- e
# may or may not need next statement. In this case it doesn't matter.
environment(my.plot.acf) <- e
e$plot.acf <- my.plot.acf
acf(1:10)
In this case we have not placed the revised acf
in e
as in the proto example but only set its parent. In fact, placing the revised acf
into e
or the proto object is not strictly necessary but was only done in the proto case because proto has the side effect of resetting the environment and it was that side effect we were after. On the other hand it is necessary to put the revised plot.acf
in e
or the proto object in order that it be encountered prior to the original one.
You might want to read this paper and, in particular, the section on Proxies starting page 21 since the technique shown here is an example of a proxy object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With