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)
Related
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
>
Anyone know if the following can be achieved in R specifically S4
foo <- setClass("foo", contains = "matrix")
foo <- function(m = matrix(1:9, nrow = 3)) new("foo", m)
setMethod("dim", signature = "foo",
function(x) {
dd <- dim(x#.Data)
cat("foo dims: ")
return(dd)
}
)
# followed by
bar <- foo()
How or can it be achieved to distinguish between ...
dim(bar)
# which gives
foo dims: [1] 3 3
# and calling dim to assign the return value to a variable
# ie this call
bardims <- dim(bar)
# which does
foo dims:
# but I don't want it to produce any cat output to the console/screen
in the second case I would like to suppress the cat(....) part of the original "dim,foo-method".
I would not mind defining something like setMethod('<-dim', 'foo', function(.... but I guess that is not available?
Info: I am using R-4.0.5 here
It's generally not a great idea to use cat() to spit out messages in function. It gives users very little control over how they display and makes it very difficult to grab those values should they ever want them.
A possible alternative is to annotate the response with a custom class that will output a message only when print()-ed. Hence it will not show up during assignment because those results are returned invisibly.
Here's an S3 class that can help
annotate_value <- function(val, msg) {
attr(val, "message") <- msg
class(val) <- c("annotated", class(val))
val
}
print.annotated <- function(x) {
class(x) <- setdiff(class(x), "annotated")
cat(attr(x, "message"))
attr(x, "message") <- NULL
print(x)
}
And then you use it like
setMethod("dim", signature = "foo",
function(x) {
dd <- dim(x#.Data)
annotate_value(dd, "foo dims:")
}
)
Then when you run your code, you get the desired output
bar <- foo()
dim(bar)
# foo dims:[1] 3 3
bardims <- dim(bar)
#
I'm trying to understand how to build objects with vectors. I thought this was straightforwards, but then had trouble when I used c() on my object.
Our object has two attributes, x and descriptor, both strings in this case (my object will have attributes with differing types). We've built a constructor, new_toy_vector. I haven't built a convenience function in this example yet.
new_toy_vector <- function(
x = character(),
descriptor = character()) {
vctrs::vec_assert(x,character())
vctrs::vec_assert(descriptor, character())
vctrs::new_vctr(x,
descriptor = descriptor,
class = "toy_vector")
}
format.toy_vector <- function(x, ...) {
paste0(vctrs::vec_data(x)," is ", attr(x, "descriptor"))
}
obj_print_data.toy_vector <- function(x) {
cat(format(x), sep = "\n")
}
c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
#> Error: No common type for `..1` <toy_vector> and `..2` <toy_vector>.
Created on 2020-04-26 by the reprex package (v0.3.0)
I then tried to create a coercion with itself unless the default method wasn't defined for some reason:
> vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) new_toy_vector()
> c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
Error: Can't convert <toy_vector> to <toy_vector>.
Any ideas what I'm missing or misunderstanding? Why can't I combine the two objects in the example?
Generally attributes are not subsetted when an object is subsetted, this is not a rule and the "names" attribute is a prominent example which doesn't follow this practice. To create an attribute that behaves like "names" you'd have to jump through hoops, and {vctrs} was designed to simplify this kind of tasks for you.
The way we do this with {vctrs} is by using records, and we won't need attributes :
Record-style objects use a list of equal-length vectors to represent individual components of the object. The best example of this is POSIXlt, which underneath the hood is a list of 11 fields like year, month, and day. Record-style classes override length() and subsetting methods to conceal this implementation detail.
Using the example in the link above as a template we can implement your case :
new_toy_vector <- function(
value = character(),
descriptor = character()) {
vctrs::vec_assert(value,character())
vctrs::vec_assert(descriptor, character())
vctrs::new_rcrd(list(value = value, descriptor = descriptor), class = "toy_vector")
}
format.toy_vector <- function(x, ...) {
value <- vctrs::field(x, "value")
descriptor <- vctrs::field(x, "descriptor")
paste0('"', value," is ", descriptor, '"')
}
v1 <- new_toy_vector(
c("Hello", "World"),
c("Foo", "Bar"))
v2 <- c(
new_toy_vector("Hello", "Foo"),
new_toy_vector("World", "Bar"))
v1
#> <toy_vector[2]>
#> [1] "Hello is Foo" "World is Bar"
identical(v1, v2)
#> [1] TRUE
v2[2]
#> <toy_vector[1]>
#> [1] "World is Bar"
Created on 2021-01-23 by the reprex package (v0.3.0)
Note that we didn't need to create a coercion method, in this case the default coercion method for records is good enough.
Add an explicit `[.toy_vector` which subsets the descriptor attribute.
Like this:
`[.toy_vector` <- function(x,i){
new_toy_vector(vec_data(NextMethod()),
descriptor = attr(NextMethod(), "descriptor")[i])
}
I'm not sure how to get attributes to 'subset' in this way using vctrs, or even if it's possible. But using this method we can basically do what vctrs does, and then some.
Bear in mind that subsetting generic will no longer call the `[.vctrs_vctr` method, so you'll lose other vctrs functionallity (such as subsetting sub-classes with vec_restore()) and may need to implement further fixes in the `[.toy_vector` method.
library(vctrs)
new_toy_vector <- function(
x = character(),
descriptor = character()) {
vec_assert(x,character())
vec_assert(descriptor, character())
new_vctr(x,
descriptor = descriptor,
class = "toy_vector")
}
format.toy_vector <- function(x, ...) {
paste0(vec_data(x)," is ", attr(x, "descriptor"))
}
obj_print_data.toy_vector <- function(x) {
cat(format(x), sep = "\n")
}
vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) {
new <- c(attr(x, "descriptor"), attr(y, "descriptor"))
new_toy_vector(descriptor = new)
}
vec_cast.toy_vector.toy_vector <- function(x, to, ...) {
new_toy_vector(vec_data(x),
attr(to, "descriptor"))
}
`[.toy_vector` <- function(x,i){
new_toy_vector(vec_data(NextMethod()),
descriptor = attr(NextMethod(), "descriptor")[i])
}
c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar")) -> tmp
tmp
#> <toy_vector[2]>
#> Hello is Foo
#> World is Bar
tmp[1]
#> <toy_vector[1]>
#> Hello is Foo
tmp[2]
#> <toy_vector[1]>
#> World is Bar
Created on 2021-01-19 by the reprex package (v0.3.0)
I tried your code and I got a more informative error message:
Error: Can't combine `..1` <toy_vector> and `..2` <toy_vector>.
x Some attributes are incompatible.
ℹ The author of the class should implement vctrs methods.
ℹ See <https://vctrs.r-lib.org/reference/faq-error-incompatible-attributes.html>.
Run `rlang::last_error()` to see where the error occurred.
https://vctrs.r-lib.org/reference/faq-error-incompatible-attributes.html
If you go to the page about the error, the answer is there: vctrs does not know by default how to combine custom attributes. Your vectors have different attributes: Foo and Bar.
If you try
a <- new_toy_vector("Hello", "Foo")
b <- new_toy_vector("World", "Foo")
c(a, b)
this will work.
To provide some context why I put a bounty on this question (and to give a bad answer to the question); I can get concatenation to work, but this causes trouble in other areas. So obviously something isn't right, but what?
library(vctrs)
new_toy_vector <- function(
x = character(),
descriptor = character()) {
vec_assert(x,character())
vec_assert(descriptor, character())
new_vctr(x,
descriptor = descriptor,
class = "toy_vector")
}
format.toy_vector <- function(x, ...) {
paste0(vec_data(x)," is ", attr(x, "descriptor"))
}
obj_print_data.toy_vector <- function(x) {
cat(format(x), sep = "\n")
}
vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) {
new <- c(attr(x, "descriptor"), attr(y, "descriptor"))
new_toy_vector(descriptor = new)
}
vec_cast.toy_vector.toy_vector <- function(x, to, ...) {
new_toy_vector(vec_data(x),
attr(to, "descriptor"))
}
z <- c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
print(z)
#> <toy_vector[2]>
#> Hello is Foo
#> World is Bar
# Subsetting doesn't work properly
z[2]
#> <toy_vector[1]>
#> World is Foo
#> World is Bar
Created on 2021-01-18 by the reprex package (v0.3.0)
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.
This is one of those "there has to be a function for this" questions. It's not that big a deal, but it's just annoying enough that every time I rename an object I wonder if there's a better way.
Suppose I capitalize an object that I've created and realize I'd rather have it uncapitalized:
# Create test data
X <- runif(100)
# Rename the object
x <- X
rm(X)
Is there a one-command way of doing this (that also avoids the re-copy for memory/speed reasons)? There are a few commands named rename in various packages but they all work on elements within a list, rather than on the list (or other object) itself.
I don't know of a built in way to do this but you could easily write your own function to do something along these lines. For instance this does just that without any checking to make sure the object exists or whether or not there is already an object named what you want to rename to.
mv <- function(x, y){
x_name <- deparse(substitute(x))
y_name <- deparse(substitute(y))
assign(y_name, x, pos = 1)
rm(list = x_name, pos = 1)
invisible()
}
Some example use
> x <- 3
> x
[1] 3
> y
Error: object 'y' not found
> mv(x, y)
> x
Error: object 'x' not found
> y
[1] 3
Edit: For those that didn't follow the link in the comments here is a version written by Rolf Turner that does some checking to make sure the object we want to move actually exists and asks us if we want to overwrite an existing object if the new name already has an object in it.
mv <- function (a, b) {
anm <- deparse(substitute(a))
bnm <- deparse(substitute(b))
if (!exists(anm,where=1,inherits=FALSE))
stop(paste(anm, "does not exist.\n"))
if (exists(bnm,where=1,inherits=FALSE)) {
ans <- readline(paste("Overwrite ", bnm, "? (y/n) ", sep = ""))
if (ans != "y")
return(invisible())
}
assign(bnm, a, pos = 1)
rm(list = anm, pos = 1)
invisible()
}