I want to detect if a variable is missing inside a function without calling the missing() function. I've found two alternatives, but they both seem crude.
Alternative 1
It seems that a variable that is missing has the environmnent class "name" but it seems intuitively wrong to use this construct:
a <- function(a, b){
e <- environment()
if(class(e[["b"]]) == "name")
e$b <- a
print(b)
}
Alternative 2
I guess a possible solution is to use parse and eval but it seems just as crude as the previous solution:
a <- function(a, b){
e <- environment()
if(eval(parse(text = sprintf("missing(%s)", "b"))))
e$b <- a
print(b)
}
Background
I need this as I'm changing the API and I would like to loop over all the old argument names within the ... and send a warning that the user should update to the new parameter names. This is why missing() doesn't work, my current setup is:
# Warnings due to interface changes in 1.0
API_changes <-
c(rowname = "rnames",
headings = "header",
halign = "align.header")
dots <- list(...)
fenv <- environment()
for (i in 1:length(API_changes)){
old_name <- names(API_changes)[i]
new_name <- API_changes[i]
if (old_name %in% names(dots)){
if (class(fenv[[new_name]]) == "name"){
fenv[[new_name]] <- dots[[old_name]]
dots[[old_name]] <- NULL
warning("Deprecated: '", old_name, "'",
" argument is now '", new_name ,"'",
" as of ver. 1.0")
}else{
stop("You have set both the old parameter name: '", old_name, "'",
" and the new parameter name: '", new_name, "'.")
}
}
}
Gosh,-- do we really have to point you to the appropriate fortune entry concerning eval(parse()) ?
Anyway,what's wrong with looping over the contents of dots<-list(...) ? It's not a time-pig by any means.
But my fundamental response is: you've made a mistake by allowing valid or invalid arguments within the ... entries. I don't know why you set up your previous function that way, but it's probably much cleaner, and safer, in the long run to eliminate this construct from your updated release. There's a reason that functions&packages come with help pages. Much as I approve of back-compatibility, I don't think you're doing anyone a favor here. Further, it's not clear to me how or why you'd want a required argument to be passed via ... . And if it's not required, then you don't want to emulate missing in the first place.
Your users will very quickly :-) realize that they've got invalid argument names. Regardless of whether you provide this transitional set of warning messages, they'll either adapt, or emigrate from your code to other options.
Related
I'd like to capture all arguments and values and return a named list. I'm having trouble with a situation where the value supplied is a function call.
The following produces an object of class "call", which is inconvenient to me, because I want to call paste on it:
fun1 = function(a = 1) as.list(match.call()[-1])
value1 = fun1(a = letters[1:2])
class(value1[[1]])
[1] "call"
paste(value1[[1]], collapse = " - ")
[1] "[ - letters - 1:2" #not useful to me
As a workaround, I can call eval to get the character vector created by c (the lapply function is there to illustrate that when having multiple arguments, eval would be called on all of them):
fun2 = function(a = 1) lapply(as.list(match.call()[-1]), eval)
value2 = fun2(a = letters[1:2])
class(value2[[1]])
[1] "character"
paste(value2[[1]], collapse = " - ")
[1] "a - b" #that's what I want
Is there a better way to do this? Calling eval on a bunch of things just to get the values seems a bit weird to me.
EDIT: The idea behind this is that I would like to pass a list of arguments to a function (which accepts a named list for one of it's arguments) within the original function. I'd like to capture values provided by the user, and default ones (for arguments where the user did not provide anything).
I learned elsewhere that I can get all of that by a combination of match.call and formals. But then, say I need to pass that to the query argument of the httr::GET function, and do some processing on it before I do so (such as adding " - " between "a" and "b"). I then end up with something like "[ - letters - 1:2" instead of "a - b".
I sense that if the answer is using eval, then I am probably asking the wrong question.
I sense that you are looking for something more general, so not sure if this is entirely what you are looking for but its simpler and gives you the desired result. The critical piece here is do.call()
fun1 = function(a = 1) {
L1 <- as.list(match.call())
do.call(paste0, list(L1$a, sep="", collapse=" - "))
}
value1 = fun1(a = letters[1:2])
Well, I think you need to decide which part of your code needs evaluation and what needs not.
It's not entirely clear from your example how general you want to go, but your example-question can be solved by a simple list(), you only need a custom function for providing defaults:
myfun <- function(a=1) list(a=a)
value <- myfun(a=letters[1:2]))
paste(value[[1]], collapse = " - ")
# Basically: value <- list(a=letters[1:2])), or paste(letters[1:2], collapse= " - ")
Generally, you use match.call() without any arguments to find out in what way your function was called. Sometimes it's useful to know whether fun(a=c('a', 'b')) was called, or fun1(a = letters[1:2]), so match.call tells you this, without evaluating anything.
So if you want to actually do something with your arguments, just call them directly, and you can later pass them on to another function
mypaste <- function(..., sep=' -CustomSep- ', collapse=' -Mycollapse- ', prefix='Value:') {
if(length(list(...))>0) {
paste(prefix, ..., sep=sep, collapse=collapse)
} else {
text <- character(0)
}
}
This function is just a variation on paste, but you can make it extensive as you want.
And I get the impression that you want a general case where you match your arguments to the arguments of another function, but to answer that question I'd need to know more about what exactly you are trying to accomplish.
I love the package but I was wondering how I could change one rule from the tidyverse style: I'd like to keep "=" instead of "<-" for assignment.
I've read that note: http://styler.r-lib.org/articles/customizing_styler.html#implementation-details
But I still don't get how to simply change that rule.
I've tried the very naive:
library(styler)
force_assignment_op <- function (pd)
{
to_replace <- pd$token == "LEFT_ASSIGN"
pd$token[to_replace] <- "EQ_ASSIGN"
pd$text[to_replace] <- "="
pd
}
tidyverse_style()$token$force_assignment_op = force_assignment_op
But get the following error:
Error in tidyverse_style()$token$force_assignment_op =
force_assignment_op :
invalid (NULL) left side of assignment
I would like to modify it in a way that I can simply run the styler addin afterwards.
The problem is that tidyverse_style()$token is a list, not an environment, so you can't modify it. (Well, you can modify it, but you're modifying a copy, not the original.)
You need to write your own function to replace the tidyverse_style function, and use it instead. For example, assuming you keep your force_assignment_op function:
LaSy_style <- function(...) {
ts <- tidyverse_style(...)
ts$token$force_assignment_op <- force_assignment_op
ts
}
Then
style_text(c("ab <- 3", "a <-3"), strict = FALSE, style = LaSy_style)
(one of the examples from ?tidyverse_style) will print
ab = 3
a = 3
(This is ugly, the original tidyverse_style is better, but I won't stop you.)
My problem is as follows: I'm trying to write a function that sets a collection of attributes on an object in a given environment. I'm trying to mimic a metadata layer, like SAS does, so you can set various attributes on a variable, like label, decimal places, date format, and many others.
Example:
SetAttributes(object = "list$dataframe$column", label="A label", width=20, decDigits=2,
dateTimeFormat="....", env=environment())
But I have to set attributes on different levels of objects, say:
comment(list$dataframe$column) <- "comment on a column of a dataframe in a list"
comment(dataframe$column) <- "comment on a column of a dataframe"
comment(list) <- "comment on a list/dataframe/vector"
Alternatively it can be done like this:
comment("env[[list]][[dataframe]][[column]]) <- "text"
# (my function recognizes both formats, as a variable and as a string with chain of
# [[]] components).
So I have implemented it this way:
SetAttributes <- function(varDescription, label="", .........., env=.GlobalEnv) {
parts <- strsplit( varDescription, "$", fixed=TRUE)[[1]]
if(length(parts) == 3) {
lst <- parts[1]
df <- parts[2]
col <- parts[3]
if(!is.na(label)) comment(env[[lst]][[df]][[col]]) <- label
if(!is.na(textWidth)) attr(env[[lst]][[df]][[col]], "width") <- textWidth
....
} else if(length(parts) == 2) {
df <- varTxtComponents[1]
col <- varTxtComponents[2]
if(!is.na(label)) comment(env[[df]][[col]]) <- label
if(!is.na(textWidth)) attr(env[[df]][[col]], "width") <- textWidth
....
} else if(length(parts) == 1) {
....
You see the problem now: I have three blocks of similar code for length(parts) == 3, 2 and 1
When I tried to automatize it this way:
path <- c()
sapply(parts, FUN=function(comp){ path <<- paste0(path, "[[", comp, "]]") )}
comment(eval(parse(text=paste0(".GlobalEnv", path)))) <- "a comment"
I've got an error:
Error in comment(eval(parse(text = paste0(".GlobalEnv", path)))) <- "a comment" :
target of assignment expands to non-language object
Is there any way to get an object on any level and set attributes for it not having a lot of repeated code?
PS: yes, I heard thousand times that changing external variables from inside a function is an evil, so please don't mention it. I know what I want to achieve.
Just to make sure you hear it 1001 times, it's a very bad idea for a function to have side effects like this. This is a very un R-like way to program something like this. If you're going to write R code, it's better to do things the R way. This means returning modified objects that can optionally be reassigned. This would make life much easier.
Here's a simplified version which only focuses on the comment.
SetComment <- function(varDescription, label=NULL, env=.GlobalEnv) {
obj <- parse(text= varDescription)[[1]]
eval(substitute(comment(X)<-Y, list(X=obj, Y=label)), env)
}
a<-list(b=4)
comment(a$b)
# NULL
SetComment("a$b", "check")
comment(a$b)
# [1] "check"
Here, rather than parsing and splitting the string, we build an expression that we evaluate in the proper context. We use substitute() to pop in the values you want to the actual call.
R allows for assignment via <- and =.
Whereas there a subtle differences between both assignment operators, there seems to be a broad consensus that <- is the better choice than =, as = is also used as operator mapping values to arguments and thus its use may lead to ambiguous statements. The following exemplifies this:
> system.time(x <- rnorm(10))
user system elapsed
0 0 0
> system.time(x = rnorm(10))
Error in system.time(x = rnorm(10)) : unused argument(s) (x = rnorm(10))
In fact, the Google style code disallows using = for assignment (see comments to this answer for a converse view).
I also almost exclusively use <- as assignment operator. However, the almost in the previous sentence is the reason for this question. When = acts as assignment operator in my code it is always accidental and if it leads to problems these are usually hard to spot.
I would like to know if there is a way to turn off assignment via = and let R throw an error any time = is used for assignment.
Optimally this behavior would only occur for code in the Global Environment, as there may well be code in attached namespaces that uses = for assignment and should not break.
(This question was inspired by a discussion with Jonathan Nelson)
Here's a candidate:
`=` <- function(...) stop("Assignment by = disabled, use <- instead")
# seems to work
a = 1
Error in a = 1 : Assignment by = disabled, use <- instead
# appears not to break named arguments
sum(1:2,na.rm=TRUE)
[1] 3
I'm not sure, but maybe simply overwriting the assignment of = is enough for you. After all, `=` is a name like any other—almost.
> `=` <- function() { }
> a = 3
Error in a = 3 : unused argument(s) (a, 3)
> a <- 3
> data.frame(a = 3)
a
1 3
So any use of = for assignment will result in an error, whereas using it to name arguments remains valid. Its use in functions might go unnoticed unless the line in question actually gets executed.
The lint package (CRAN) has a style check for that, so assuming you have your code in a file, you can run lint against it and it will warn you about those line numbers with = assignments.
Here is a basic example:
temp <- tempfile()
write("foo = function(...) {
good <- 0
bad = 1
sum(..., na.rm = TRUE)
}", file = temp)
library(lint)
lint(file = temp, style = list(styles.assignment.noeq))
# Lint checking: C:\Users\flodel\AppData\Local\Temp\RtmpwF3pZ6\file19ac3b66b81
# Lint: Equal sign assignemnts: found on lines 1, 3
The lint package comes with a few more tests you may find interesting, including:
warns against right assignments
recommends spaces around =
recommends spaces after commas
recommends spaces between infixes (a.k.a. binary operators)
warns against tabs
possibility to warn against a max line width
warns against assignments inside function calls
You can turn on or off any of the pre-defined style checks and you can write your own. However the package is still in its infancy: it comes with a few bugs (https://github.com/halpo/lint) and the documentation is a bit hard to digest. The author is responsive though and slowly making improvements.
If you don't want to break existing code, something like this (printing a warning not an error) might work - you give the warning then assign to the parent.frame using <- (to avoid any recursion)
`=` <- function(...){
.what <- as.list(match.call())
.call <- sprintf('%s <- %s', deparse(.what[[2]]), deparse(.what[[3]]))
mess <- 'Use <- instead of = for assigment '
if(getOption('warn_assign', default = T)) {
stop (mess) } else {
warning(mess)
eval(parse(text =.call), envir = parent.frame())
}
}
If you set options(warn_assign = F), then = will warn and assign. Anything else will throw an error and not assign.
examples in use
# with no option set
z = 1
## Error in z = 1 : Use <- instead of = for assigment
options(warn_assign = T)
z = 1
## Error in z = 1 : Use <- instead of = for assigment
options(warn_assign = F)
z = 1
## Warning message:
## In z = 1 : Use <- instead of = for assigment
Better options
I think formatR or lint and code formatting are better approaches.
I'm looking for a general practice how to check a parameter was defined in a function.
I came up with these three ideas. Which one is the proper way of doing it?
Unfortunately, the third one is not working. substitute() is working differently in a function and I couldn't figure it out how to use it properly.
file.names <- list(
cov.value <- "cov.rds",
plot.name <- "plot.pdf"
)
test1 <- function(file.names){
is.save <- !missing(file.names)
}
test2 <- function(file.names = NULL) {
is.save <- !is.null(file.names)
}
test3 <- function(file.names = NULL) {
is.save <- exists(as.character(substitute(file.names)))
}
I personally think the second approach with a default value is MUCH easier to use & understand. (And the third approach is just really bad)
...especially when you are writing a wrapper function that needs to pass an argument to it. How to pass a "missing"-value is not obvious!
wraptest1 <- function(n) {
file.names <- if (n > 0) sample(LETTERS, n)
else alist(a=)[[1]] # Hacky way of assigning 'missing'-value
print(test1(file.names))
}
wraptest1(2) # TRUE
wraptest1(0) # FALSE
wraptest2 <- function(n) {
file.names <- if (n > 0) sample(LETTERS, n)
else NULL # Much easier to read & understand
print(test2(file.names))
}
wraptest2(2) # TRUE
wraptest2(0) # FALSE
[granted, there are other ways to work around passing the missing value, but the point is that using a default value is much easier...]
Some default values to consider are NULL, NA, numeric(0), ''
It's generally a good idea to look at code from experienced coders---and R itself has plenty of examples in the R sources.
I have seen both your first and second example being used. The first one is pretty idiomatic; I personally still use the second more often by force of habit. The third one I find too obscure.