Using quotation to programmatically create a S3 generic - r

I want to create an S3 generic in the global environment from a function. I took a look at R.methodsS3::setGenericS3.default and came up with the following:
create_generic <- function(nm) {
src <- sprintf("%s <- function(obj, ...) UseMethod(\"%s\")", nm, nm)
expr <- parse(text = src)
print(expr)
eval(expr, env = .GlobalEnv)
}
create_generic("cat")
#> expression(cat <- function(obj, ...) UseMethod("cat"))
cat
#> function (obj, ...)
#> UseMethod("cat")
This works how I'd like it to. However, I've been trying to make this work using quotation and I'm stuck:
library(rlang)
create_generic2 <- function(nm) {
expr <- expr(!!sym(nm) <- function(obj, ...) UseMethod(!!nm))
print(expr)
eval(expr, env = .GlobalEnv)
}
create_generic2("dog")
#> dog <- function(obj, ...) UseMethod("dog")
dog
#> function(obj, ...) UseMethod(!!nm)
This makes use of tidyeval since that's what I'm familiar with, but I'd like to see what this looks like in base R.
I'm interested in any version of this that works without the string manipulation in create_generic.

In base R:
create_generic <- function(fun_name) {
new_fun <- function(obj, ...) UseMethod(fun_name)
assign(fun_name, new_fun, envir = .GlobalEnv)
}
cat("hi\n")
# hi
create_generic("cat")
cat("hi\n")
# Error in UseMethod(fun_name) :
# no applicable method for 'cat' applied to an object of class "character"
cat.character <- base::cat
cat("hi\n")
# hi

You can also use expr_interp() to use unquoting operators within functions:
create_generic <- function(name, env = globalenv()) {
gen <- expr_interp(function(obj, ...) {
UseMethod(!!name)
})
environment(gen) <- env
assign(name, gen, envir = env)
}
The prefix is expr_ because it is (internally) generic over expression wrappers, e.g. formulas and functions.

Related

Export manually missed globals object with future

I defined a function f in a package that takes data and an R expression as input and then applies the user-defined expression on the data. Here's an example of the function's use:
f <- function(data, expr) {
expr <- substitute(expr)
eval(expr, envir = data)
}
data <- data.frame(a = 1:2, b = 3:4)
f(data, mean(a))
#> [1] 1.5
The problem arises with the parallel version of this function using explicit futures and user-defined object. Here a toy version:
library(future)
f <- function(data, expr) {
expr <- substitute(expr)
y <- future::future(eval(expr, envir = data))
future::value(y)
}
data <- data.frame(a = 1:2, b = 3:4)
myfun <- function(x){sum(sqrt(x))}
plan(sequential)
f(data, myfun(a))
#> [1] 2.414214
plan(multiprocess)
f(data, myfun(a))
#> Error in myfun(a) : impossible to find function "myfun"
The problem is that myfun cannot trivially be found by future and thus must be exported manually. I'm able to fix this issue by analyzing expr with future::getGlobalsAndPackages and then manually adding objects:
future::future(..., globals = structure(TRUE, add = globals))
I'm wondering if there is a better/good way to do that since it looks like a hack to me.
I finally found that the ellipsis in plan propagates to future
plan(multiprocess, globals = myfun)

R: how to find what S3 method will be called on an object?

I know about methods(), which returns all methods for a given class. Suppose I have x and I want to know what method will be called when I call foo(x). Is there a oneliner or package that will do this?
The shortest I can think of is:
sapply(class(x), function(y) try(getS3method('foo', y), silent = TRUE))
and then to check the class of the results... but is there not a builtin for this?
Update
The full one liner would be:
fm <- function (x, method) {
cls <- c(class(x), 'default')
results <- lapply(cls, function(y) try(getS3method(method, y), silent = TRUE))
Find(function (x) class(x) != 'try-error', results)
}
This will work with most things but be aware that it might fail with some complex objects. For example, according to ?S3Methods, calling foo on matrix(1:4, 2, 2) would try foo.matrix, then foo.numeric, then foo.default; whereas this code will just look for foo.matrix and foo.default.
findMethod defined below is not a one-liner but its body has only 4 lines of code (and if we required that the generic be passed as a character string it could be reduced to 3 lines of code). It will return a character string representing the name of the method that would be dispatched by the input generic given that generic and its arguments. (Replace the last line of the body of findMethod with get(X(...)) if you want to return the method itself instead.) Internally it creates a generic X and an X method corresponding to each method of the input generic such that each X method returns the name of the method of the input generic that would be run. The X generic and its methods are all created within the findMethod function so they disappear when findMethod exits. To get the result we just run X with the input argument(s) as the final line of the findMethod function body.
findMethod <- function(generic, ...) {
ch <- deparse(substitute(generic))
f <- X <- function(x, ...) UseMethod("X")
for(m in methods(ch)) assign(sub(ch, "X", m, fixed = TRUE), "body<-"(f, value = m))
X(...)
}
Now test it. (Note that the one-liner in the question fails with an error in several of these tests but findMethod gives the expected result.)
findMethod(as.ts, iris)
## [1] "as.ts.default"
findMethod(print, iris)
## [1] "print.data.frame"
findMethod(print, Sys.time())
## [1] "print.POSIXct"
findMethod(print, 22)
## [1] "print.default"
# in this example it looks at 2nd component of class vector as no print.ordered exists
class(ordered(3))
## [1] "ordered" "factor"
findMethod(print, ordered(3))
## [1] "print.factor"
findMethod(`[`, BOD, 1:2, "Time")
## [1] "[.data.frame"
I use this:
s3_method <- function(generic, class, env = parent.frame()) {
fn <- get(generic, envir = env)
ns <- asNamespace(topenv(fn))
tbl <- ns$.__S3MethodsTable__.
for (c in class) {
name <- paste0(generic, ".", c)
if (exists(name, envir = tbl, inherits = FALSE)) {
return(get(name, envir = tbl))
}
if (exists(name, envir = globalenv(), inherits = FALSE)) {
return(get(name, envir = globalenv()))
}
}
NULL
}
For simplicity this doesn't return methods defined by assignment in the calling environment. The global environment is checked for convenience during development. These are the same rules used in r-lib packages.

eval inside gsubfn inside sub function: object not found

Give the two functions
subfun <- function(txt)
gsubfn::gsubfn("§([^§]+)§", ~eval(parse(text=x)), txt)
topfun <- function(id = 1L)
subfun("Hello §id§ world!")
The following (1.) should yield "Hello 1 world!"but throws an error instead:
topfun()
# Error in eval(expr, envir, enclos) : object 'id' not found
These two (2.) & (3.) work as expected:
id <- 2L
topfun()
# [1] "Hello 2 world!"
topfun2 <- function(id = 1L)
gsubfn::gsubfn("§([^§]+)§", ~eval(parse(text=x)), "Hello §id§ world!")
topfun2()
# [1] "Hello 1 world!"
How can I make (1.) work?
I tried several environment() and parent.frame() variations with the envir parameter of eval and gsubfn, including passing topfun's environment to subfun via the ellipsis argument. All to no success. (Not that I had greater knowledge of what's going on under the hood. But I would have expected R to go up one parent environment after another to look for id...)
I'm using R version 3.3.0 and gsubfn package version 0.6.6.
Thanks in advance!
I am no expert at this but the problem seems to be the use of a formula as replacement in gsubfun. At least I am unable to pass an environment to eval if it is in a formula.
subfun_2 <- function(txt){
ev <- parent.frame() # the environment in which subfun_2 was called
gsubfn::gsubfn("§([^§]+)§", ~eval(parse(text=x), envir = ev), txt)
}
topfun_2 <- function(id = 1L) subfun_2("Hello §id§ world!")
topfun_2()
# Error in eval(parse(text = x), envir = ev) :
# argument "ev" is missing, with no default
If you use a function instead it works as expected:
subfun_3 <- function(txt){
ev <- parent.frame()
gsubfn::gsubfn("§([^§]+)§", function(x)eval(parse(text=x), envir = ev), txt)
}
topfun_3 <- function(id = 1L) subfun_3("Hello §id§ world!")
topfun_3()
# Hello 1 world!

Evaluate an expression within an environment inside a function

Consider:
guy <- new.env(FALSE)
guy$stuff <- mean
guy$lib <- library
guy$stole_this_data <- mtcars
ls(guy)
How can I evaluate an expression within an environment inside a function?
For instance I can do with(guy, args(stuff)) to the below and return:
> with(guy, args(stuff))
function (x, ...)
NULL
But within a functon:
foo <- function(env, fun) {
with(env, args(fun))
}
foo(guy, stuff)
## > foo(guy, stuff)
## Error in args(fun) : could not find function "stuff"
Try this:
> foo <- function(env, fun) eval(substitute(args(fun)), env)
> foo(guy, stuff)
function (x, ...)
NULL
ADDED. Regarding the comment below here is an example where zz is not in env or its ancestors (but is in foo2 and in f, the caller of foo2) and it does give a not found error as the comment wished:
> foo2 <- function(env, fun, zz = 1) eval(substitute(fun), env)
> f <- function() { zz <- 100; foo2(guy, zz+1) }
> f()
Error in eval(expr, envir, enclos) : object 'zz' not found
If you want to continue to use the with construct, this is an alternative:
foo <- function(env, fun) {
fun <- substitute(fun)
eval(bquote(with(env, {
.(fun)
})))
}

Function to assign a value in a new environment in R

I'm trying to write a function 'exported' in R that will assign a value to a name in a desired environment (say .GlobalEnv). I'd like to use the following syntax.
# desired semantics: x <- 60
exported(x) <- 60
# ok if quotes prove necessary.
exported("x") <- 60
I've tried several variations. Most basically:
`export<-` <- function(x, obj) {
call <- as.list(match.call())
elem <- as.character(call[[2]])
assign(elem, obj, .GlobalEnv)
get(elem, .GlobalEnv)
}
exported(x) <- 50
The foregoing gives an error about the last argument being unused. The following complains that "object 'x' is not found."
setGeneric("exported<-", function(object, ...) {
standardGeneric("exported<-")
})
setReplaceMethod("exported", "ANY", function(object, func) {
call <- as.list(match.call())
name <- as.character(call$object)
assign(name, func, other.env)
assign(name, func, .GlobalEnv)
get(name, .GlobalEnv)
})
exported(x) <- 50
The above approach using a character vector in place of a name yields "target of assignment expands to non-language object."
Is this possible in R?
EDIT: I would actually like to do more work inside 'exported.' Code was omitted for brevity. I also realize I can use do something like:
exported(name, func) {
...
}
but am interested in seeing if my syntax is possible.
I can't understand why you wouldn't use assign?
assign( "x" , 60 , env = .GlobalEnv )
x
[1] 60
The env argument specifies the environment into which to assign the variable.
e <- new.env()
assign( "y" , 50 , env = e )
ls( env = e )
[1] "y"

Resources