I want to create a custom log function, that would get used in other functions. I am having issues with the custom function where arguments don't seem to flow through to the inner log function. My custom log function is inspired by the logger package but I am planning to expand this usage a bit further (so logger doesn't quite meet my needs)
log_fc <- function(type = c("INFO", "ERROR"), ...) {
print(
glue::glue("[{type} {Sys.time()}] ", ...)
)
}
Next I am planning to use log_fc in various other custom functions, one example:
test_fc <- function(forecast) {
log_fc(type = "INFO", "{forecast} is here")
#print(forecast)
}
If I test this, I get the following error:
> test_fc(forecast = "d")
Error in eval(parse(text = text, keep.source = FALSE), envir) :
object 'forecast' not found
I am not sure why argument forecast is not being picked up by the inner test_fc function. TIA
You could use the .envir argument:
log_fc <- function(type = c("INFO", "ERROR"), ...) {
env <- new.env(parent=parent.frame())
assign("type",type,env)
print(
glue::glue("[{type} {Sys.time()}] ", ...,.envir = env)
)
}
test_fc <- function(forecast) {
log_fc(type = "INFO", "{forecast} is here")
}
test_fc("My forecast")
#> [INFO 2022-12-18 12:44:11] My forecast is here
There are two things going on.
First, the name forecast is never passed to log_fc. The paste solution never needs the name, it just needs the value, so it still works. You'd need something like
log_fc(type = "INFO", "{forecast} is here", forecast = forecast)
to get the name into log_fc.
The second issue is more complicated. It's a design decision in many tidyverse functions. They want to be able to have code like f(x = 3, y = x + 1) where the x in the second argument gets the value that was bound to it in the first argument.
Standard R evaluation rules would not do that; they would look for x in the environment where f was called, so f(y = x + 1, x = 3) would bind the same values in the function as putting the arguments in the other order.
The tidyverse implementation of this non-standard evaluation messes up R's internal handling of .... The workaround (described here: https://github.com/tidyverse/glue/issues/231) is to tell glue() to evaluate the arguments in a particular location. You need to change your log function to fix this.
One possible change is shown below. I think #Waldi's change is actually better, but I'll leave this one to show a different approach.
log_fc <- function(type = c("INFO", "ERROR"), ...) {
# Get all the arguments from ...
args <- list(...)
# The unnamed ones are messages, the named ones are substitutions
named <- which(names(args) != "")
# Put the named ones in their own environment
e <- list2env(args[named])
# Evaluate the substitutions in the new env
print(
glue::glue("[{type} {Sys.time()}] ", ..., .envir = e)
)
}
test_fc <- function(forecast) {
log_fc(type = "INFO", "{forecast} is here", forecast = forecast)
}
test_fc(forecast = "d")
#> [INFO 2022-12-18 06:25:29] d is here
Created on 2022-12-18 with reprex v2.0.2
The reason for this is that when your test_fc function connects to the log_fc function, the forecats variable wouldn't be able to be found, because it's not a global function; thus, you can't access it from the other function.
The way to fix this is by defining a global variable:
log_fc <- function(type = c("INFO", "ERROR"), ...) {
print(
glue::glue("[{type} {Sys.time()}] ", ...)
)
}
test_fc <- function(forecast) {
forecast <<- forecast
log_fc(type = "INFO", "{forecast} is here")
}
print(test_fc(forecast = "d"))
Output:
d is here
Since you're already using glue you could use another glue::glue in test_fc to accomplish the pass-through, such as:
log_fc <- function(type = c("INFO", "ERROR"), ...) {
print(
glue::glue("[{type} {Sys.time()}] ", ...)
)
}
test_fc <- function(forecast) {
log_fc(type = "INFO", glue::glue("{forecast} is here"))
}
which yields
> test_fc('arctic blast')
[INFO 2022-12-21 15:56:18] arctic blast is here
>
Related
For a package all the internal functions start with a ".".
Example .internalfunciton() and externalfunction(). This is used for quick namespace exporting.
Now I am trying to write a internal s3 method. There seems to be problems with it working with the dot at the start of the function name.
Here is some examples I have come up with to test it:
test <- function(x,...) UseMethod("test", x)
test.class <- function(x, ...) {
print("works like a charm")
}
.dottest <- function(x,...) UseMethod(".dottest", x)
.dottest.class <- function(x, ...) {
print("works like a charm even with a dot")
}
When I test it it ends up like this.
item <- 5
class(item) <- "class"
class(item)
#> [1] "class"
BrailleR:::test(item)
#> Error in UseMethod("test", x): no applicable method for 'test' applied to an object of class "class"
BrailleR:::.dottest(item)
#> Error in UseMethod(".dottest", x): no applicable method for '.dottest' applied to an object of class "class"
This happens when I load the functions locally or use the load_all method afterplacing that test function code in the package or even after installing it as this particular version shows.
Edit: As pointed out in comments some of these tests were invalid anyways due to not being put in the NAMESPACE
It feels like I am missing something with s3 generics.
Below is some context and is the actual code
.RewriteSVG = function(x, file, type) {
UseMethod(".ReWriteSVG", type)
}
.RewriteSVG.GeomLine <- function(x, file, type) {
# Adding extra 1 as this gets us into the inner line.
lineID <- paste(.GetGeomLine(type), "1", sep = ".")
svgDoc <- XML::xmlParseDoc(file)
nodes <- XML::getNodeSet(svgDoc,
paste0('//*[#id="', lineID , '"]'))
# Split the line into smaller polylines
line <- nodes[[1]]
lineAttr <- XML::xmlAttrs(line)
lineAttr <- lineAttr[!(names(lineAttr) %in% c("id", "points"))]
lineAttr <- split(lineAttr, names(lineAttr))
## Get the line points
attr <- XML::xmlGetAttr(line, 'points')
coordinates <- strsplit(attr, " ")[[1]]
## As there will always be 100 points in a graph we can just easily split them into 5 groups
nBreaks <- 6
breaks <- seq(1, 100, length.out = nBreaks) |> round()
start <- breaks[1:(nBreaks-1)]
end <- breaks[2:nBreaks]
1:(nBreaks-1) |>
lapply(function(i) {
segmentCoords <- coordinates[start[i]:end[i]]
args <- lineAttr
args$id <- paste(lineID, i, sep = ".")
args$points <- paste(segmentCoords, collapse = " ")
print(args)
newPolyline <- XML::newXMLNode('polyline', parent=line, attrs = args)
XML::addChildren(line, newPolyline)
})
# Remove old line
XML::removeNodes(line)
# Save modified svg doc
XML::saveXML(svgDoc, file=file)
}
Errors message looks like this
Error in UseMethod(".ReWriteSVG", type) :
no applicable method for '.ReWriteSVG' applied to an object of class "c('GeomLine', 'GeomPath', 'Geom', 'ggproto', 'gg')"
Which comes from the
lapply(x$layers, function(x, graphObject, file) {
.RewriteSVG(graphObject, file, x$geom)
}, graphObject = x, file = file)
I will use the following example to explain my question. But the question is not only about this specific example, but more general about meta-programming in R.
I have two specific functions to make plots
Specific function 1
draw_hists <- function(dts, indexs, title_prefix = 'sd = ') {
mapply(
function(dt, index)
{
hist(dt, main = paste(title_prefix, as.character(index)))
},
dts, indexs
)
}
plots histograms
sds <- c(0.1, 0.5, 5, 100)
raw_normals <- purrr::map(sds, ~rnorm(500, mean = 1, sd = .x))
draw_hists(raw_normals, sds)
Specific function 2
plots scatter plots of percentage ranks against raw data
draw_percentage <- function(dts, indexs, title_prefix = 'sd = ') {
mapply(
function(dt, index)
{
plot(dt, dplyr::percent_rank(dt), main = paste(title_prefix, as.character(index)))
},
dts, indexs
)
}
sds <- c(0.1, 0.5, 5, 100)
raw_normals <- purrr::map(sds, ~rnorm(500, mean = 1, sd = .x))
draw_percentage(raw_normals, sds)
Now assume I want to abstract out the general patterns of these functions and define a generic higher-order function that takes inputs of any arbitrary plotting function and its argument as an expression to be flexible enough drawing nearly whatever I want to draw. I thought something like this would work.
draw_generic <- function(dts, indexs, plfun, plfun_arguments_as_expr) {
....
}
The formal parameter plfun_arguments_as_expr would bind to an expression such like expr(dplyr::percent_rank(dt)) to make the plotting truly generic and flexible. I come up with the following solution.
draws_generic <- function(dts, indexs, plfun, title_prefix = 'sd =', ...) {
dots <- enquos(...)
mapply(
function(dt, index)
{
eval_tidy(
expr(
plfun(dt, main = paste(title_prefix, as.character(index)), !!!dots)
)
)
}
,
dts, indexs
)
}
draws_generic(raw_normals, sds, hist)
draws_generic(raw_normals, sds, plot, dplyr::percent_rank(dt))
The histogram works. But the percent_rank one gives me error
Error in x[!nas] : object of type 'closure' is not subsettable
In addition: Warning message:
In is.na(x) : is.na() applied to non-(list or vector) of type 'closure'
Called from: rank(x, ties.method = "min", na.last = "keep")
I think this might be related to the fact that the environment scope captured by enquos is global, but the expression contains a name dt for which its binding existed in local scope created by the anonymous function function(dt, index). Is this truly the reason of this error? If so, is there a neat and clean way to fix it that follows principles of "tidy evaluation"?
Update
Inspired by the comments, I modify here my question. In stead of using a pure functional abstraction to generalize procedures, what I really want is to achieve generalization by treating code as object and manipulate it freely in a R function or kind of macro programming. More precisely what I want is a draw_expression function to plot data against a given expression instead of previous draw_generic. Below are some of my attempts so far:
The 1st version plots a plotting expression with x as data argument against given data without additional indexs parameter and title. The code has been tested working.
draw_expression_1 <- function(dts, plexpr) {
plexpr <- enexpr(plexpr)
lapply(dts, eval(expr(function(x) !!plexpr)))
}
draw_expression_1(raw_normals, hist(x))
draw_expression_1(raw_normals, plot(x, dplyr::percent_rank(x))
The 2nd version adds additional indexs parameter and titles by modifying the given expression. The code has been tested working.
draw_expression_2 <- function(dts, indexs, plexpr, title_prefix = 'sd =') {
plexpr <- enexpr(plexpr)
mapply(eval(expr(function(x, index) {
UQ(rlang::call_modify(plexpr, main = quote(paste(title_prefix, as.character(index)))))
})), dts, indexs)
}
draw_expression_2(raw_normals, sds, hist(x))
draw_expression_2(raw_normals, sds, plot(x, dplyr::percent_rank(x))
The 3rd version is aimed at allowing the call expression to have any arbitrary formal parameter name instead of x. Release the assumption to be that the 1st parameter corresponds to the data to be plotted, but it can be named whatever users wished.
draw_expression_3 <- function(dts, indexs, plexpr, title_prefix = 'sd =') {
plexpr <- enexpr(plexpr)
first_arg_name <- rlang::call_args(plexpr)
mapply(eval(expr(function(first_arg_name, index) {
UQ(rlang::call_modify(plexpr, main = quote(paste(title_prefix, as.character(index)))))
})), dts, indexs)
}
draw_expression_3(raw_normals, sds, hist(x))
draw_expression_3(raw_normals, sds, plot(x, dplyr::percent_rank(x))
This prints me error:
Error in plot(x, dplyr::percent_rank(x), main = paste(title_prefix, as.character(index))) :
object 'x' not found
Apparently first_arg_name has to been unquoted in the expression. Thus I did this:
draw_expression_3 <- function(dts, indexs, plexpr, title_prefix = 'sd =') {
plexpr <- enexpr(plexpr)
first_arg_name <- rlang::call_args(plexpr)
mapply(eval(expr(function(UQ(first_arg_name), index) {
UQ(rlang::call_modify(plexpr, main = quote(paste(title_prefix, as.character(index)))))
})), dts, indexs)
}
draw_expression_3(raw_normals, sds, hist(x))
draw_expression_3(raw_normals, sds, plot(x, dplyr::percent_rank(x))
But I got weird syntax error:
Error: unexpected '}' in " }"
Now I don't understand why this happens. Any help?
Also I could not use enquo + eval_tidy here, since enquo will capture the environment of the call expression which is global, but the expression inside the function that I would like to modify and manipulate contains x which belongs to the inner scope. Thus this is not a tidy evaluation. But I am not perusing that anymore. I simply want do macro programming as freely as I can with base R plus some of convenient tools provided by rlang.
NOTE: I am not trying to do any production work. I am just trying to see the limit of this language and understand things better.
I don't know the "tidy evaluation" way to do this, but the simpler base R method is to pass a function rather than an expression. For example,
sds <- c(0.1, 0.5, 5, 100)
raw_normals <- purrr::map(sds, ~rnorm(500, mean = 1, sd = .x))
draws_generic2 <- function(dts, indexs, plfun, title_prefix = 'sd =') {
mapply(
function(dt, index)
{
plfun(dt, main = paste(title_prefix, as.character(index)))
},
dts, indexs
)
invisible(NULL)
}
par(mfrow=c(2,2))
draws_generic2(raw_normals, sds, hist)
draws_generic2(raw_normals, sds, function(dt, ...) plot(dt, dplyr::percent_rank(dt), ...))
Created on 2022-04-15 by the reprex package (v2.0.1)
I used dt in my function definition in the second example, but I could have used any variable name, e.g. this would give the same output except for the axis labels:
draws_generic2(raw_normals, sds,
function(x, ...) plot(x, dplyr::percent_rank(x), ...))
This question already has answers here:
R function with no return value
(5 answers)
Closed 1 year ago.
I have this function. I'm trying to a tibble with tweets within an object.
tuits <- function(x, y) {
x <- search_tweets(y, n=5000, include_rts = FALSE, lang = "es",
since = since, until = until) %>%
filter(screen_name != y)
}
x is the name of the object and y is the query I want to search in Twitter.
The problem is that the objects are not accessible outside the function.
tuits(Juan, "JuanPerez")
tuits(Pedro, "PedroJimenez")
For instance if I want to execute Juan, R returns this: Error: object 'Juan' not found.
What can I do, because I need those objects accessible outside the function because I want to save all of them in a XLS.
Thanks
Update: I now should have solved your problem using the ensym() and assign functions.
Essentially we want to access the global variable with the name passed to the function. In order to do this we capture its name not its contents with ensym and then we assign it using the assign function which we tell that the object we are looking for is in the global environment and has the name that we stored with ensym.
Here is a brief explanation showing how it works.
library(rlang)
f <- function(x) {
x <- ensym(x)
assign(as_string(x), 2, envir = globalenv())
}
john <- 1
f(john)
print(john)
#> [1] 2
Created on 2021-04-05 by the reprex package (v2.0.0)
For your function we would want to take this approach:
library(rlang)
tuits <- function(x, y) {
# Get the name of the variable we want to store
x <- ensym(x)
tmp <- search_tweets(y, n=5000, include_rts = FALSE, lang = "es",
since = since, until = until) %>%
filter(screen_name != y)
# Assign the value to the variable in the global environment
assign(as_string(x), tmp, envir = globalenv())
}
tuits(Juan, "JuanPerez")
# to test
print(Juan)
Old Answer (improved on in the above section)
I believe the issue here is an issue of understanding scope or environments. If an object is modifed or set within the environment used by a function or the sdcope of the function then it can only be accessed in that form within the function.
Usually the scope of a function contains the variables that are assigned inside the function statement.
Usually the way to solve this would be to return the object using return(x) and setting the function call to the object.
tuits <- function(x, y) {
x <- search_tweets(y, n=5000, include_rts = FALSE, lang = "es",
since = since, until = until) %>%
filter(screen_name != y)
return(x)
}
Juan <- tuits(Juan, "JuanPerez")
You could modify the object x using a superassignment (<<-) operation however this is usually not best practise. I will provide this solution for completeness sake.
Superassignment modifies the variable in the global scope. This however will assign the value to x not the object.
tuits <- function(x, y) {
x <<- search_tweets(y, n=5000, include_rts = FALSE, lang = "es",
since = since, until = until) %>%
filter(screen_name != y)
}
tuits(Juan, "JuanPerez")
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.
Context
I am using in R, the "elipsis" or "dots" that wrap function calls
main_function <- function(...)
If I want to evaluate once, I do
main_function <- function(...) {
res = list(...)}
It works fine
Problem
fun_A <- function(arg_A){
print(paste("I am A", paste0(round(runif(arg_A, 0,1), 2),collapse = ", ")))
}
fun_B <- function(arg_B){
print(paste("I am B", paste0(round(runif(arg_B, 1,2), 2),collapse = ", ")))
}
Here the result is evaluated once and replicate 3 times :
main_fun_wrong <- function(..., times) {
res = list(...)
replicate(times, eval(res))
}
main_fun_wrong(fun_A(1), fun_B(2), times = 3)
Here it works :
main_fun <- function(..., times) {
calls = match.call(expand.dots = FALSE)$`...`
replicate(times, lapply(1:length(calls), function(num) eval(calls[[num]])), simplify = F)
}
main_fun(fun_A(1),fun_B(2), times = 3)
But now if arg_A is an object rather than a value, it will fail finding the arg_A and arg_B in the environment.
main_fun_problem <- function(arg_A, arg_B) {
main_fun(fun_A(arg_A),fun_B(arg_B), times = 3)
}
main_fun_problem(1,2)
I got an error :
Error in fun_A(arg_A) : object 'arg_A' not found
I do not know what R do when it find list(...) the first time in first example but I just want to repeat it multiple times.
Here is my solution, any alternative will be enjoyed.
The things is to substitute the variable by it's value at the moment we call the function.
main_fun_solution <- function(arg_A, arg_B) {
eval(substitute(main_fun(fun_A(arg_A),fun_B(arg_B), times = 3), list("arg_A" = arg_A, "arg_B" = arg_B)))
}
main_fun_solution(1,2)
NB: list("arg_A" = arg_A, "arg_B" = arg_B)` makes my heart bleed (the overall solution actually)