class of expr and exprs are different in rlang in R ! Why? - r

I am not sure if this has been asked here, But I am very confused here. I am reading this awesome book called Advanced R by Hadley Wickham from here.
There is function called cement that has been described here, I have modified it little bit and trying to understand it.
library(rlang)
cement1 <- function(x) {
dots <- expr(x)
print(class(dots))
#paste(expr_name(x))
}
cement2 <- function(y,z) {
dots <- exprs(y,z)
print(class(dots))
#paste(purrr::map(dots, expr_name), collapse = " ")
}
Running the above cement1 without any parameter returns me the class of dots as "name".
However, when I run the cement2 function with additional parameter, the class returns "list", {simply putting class(expr(x)) returns "name" whereas class(exprs(x)) returns "list"}.
I am not getting my head around this as why it is printing different class returned by expr and exprs. The only difference I thought I knew about them was, one deals with one parameter, other one deals with multiple parameters, but I may be wrong, I might have missed some details.
Original Problem: So, it all started by running these two functions separately by removing the comments section in the code for both cement1 and cement2, when I run the functions Below are the output returned by them:
cement1(Hello) #Returns , Error in type_of(.x) : object 'Hello' not found
cement2(Hello) #Works very well and returns, [1] "y z"
So I tried to find the reason why cement1 failed and then printed their classes and that is when I realized , expr and exprs return different classes.
My question is:
1) Are they by design, if yes then why? Or, I am doing some horrible mistake, which I am currently unable to see.
2) Does cement1 can't work this if not , what is the correct way?
I am sorry for too long sentences, My first language is not English, hence If anything silly is there, Please let me know I shall correct it. I hope this is not a duplicate, I tried to find the answer but could not found by my own.
Thanks for any help.
R Version: 3.4.2
rlang: 0.2.0

1) Yes, the return values of expr and exprs differ by design. From the ?expr help page:
enexpr() and expr() capture a single raw expression.
enexprs() and exprs() capture a list of raw expressions including expressions contained in ....
2) expr_name() expects a quoted expression, such as what's produced by expr(). So, you need to modify your cement1 to call expr_name() on dots, not x. You can also remove paste because you are not concatenating anything.
cement1 <- function(x) {
dots <- expr(x)
# print(class(dots)) ## Commented out for clarity
expr_name(dots) ## The input to expr_name is now effectively expr(x)
}
cement1( Hello )
# "x"
Your function cement2 basically calls expr_name() on every element of the list returned by exprs(), then concatenates the results into a single string.
2a) Now that we got your cement1 working, we can improve it further. Currently, the function doesn't make any use of its input argument x. expr() simply captures the unevaluated expression, and this expression will always be x, regardless of what you name your argument:
cement1.1 <- function( completelyIgnoredName ) {
dots <- expr(x)
expr_name(dots)
}
cement1.1( Hello )
# "x"
However, if you replace expr() with enexpr(), the function will substitute the expression provided as the function's argument and capture that instead:
cement1.2 <- function(x) {
dots <- enexpr(x)
expr_name(dots)
}
cement1.2( Hello )
# "Hello"

Related

Is there a way make a parameter of a function be put in quotes in one life of code and not the other?

I'm not showing my actual code because it has many running parts, so let me just showcase what I am running into. I have this function:
paste <- function(d) {
print(paste0(d,"_test", sep =""))
}
What I wanted returned would be if I did paste(advising) for example is "advising_test".
The reason I am not just doing paste("advising") is because in the function I am writing, advising needs to be used as both in quotes in order to name a particular object and needs to be used as the name of a dataframe that I am doing another function on. Therefore, I need to figure out how in the code I can paste the parameter with quotes, so I can use the term both ways.
paste <- function(d) {
print(paste0(ensym(d),"_test", sep =""))
}
Edit : You can have a look at https://rlang.r-lib.org/reference/nse-defuse.html
Use substitute . No packages are used.
mypaste <- function(d) print(paste0(substitute(d), "_test"))
# test
mypaste(advising)
## [1] "advising_test"

why ellipsis ... have to be wrapped inside c() when used in lambda functions in purrr

May be this question termed as part-3 of this question.
I know that arguments in lambda functions when used in tidyverse especially purrr functions are written as -
. when there is only 1 argument
.x & .y when there are 2
OR ..1, ..2 so on when there are > 2 arguments
In the linked question, I learnt that if all the arguments have to be simultaneously passed we may use ellipsis i.e. ....
But my question, why such ... work only when wrapped inside a c() and doesn't work when used as such. In the below two syntaxes, second one works while first one doesn't?
#1 this doesn't work
pmap_df(iris[1:4], ~...)
Error in .f(Sepal.Length = .l[[1L]][[i]], Sepal.Width = .l[[2L]][[i]], :
'...' used in an incorrect context
#2 this however, works and returns the first argument after converting it to a tibble
pmap_df(iris[1:4], ~ c(...))
#see
identical(pmap_df(iris[1:4], ~c(...)), iris[1:4] %>% as_tibble())
[1] TRUE
Can someone explain?
The offical documentation has this to be said about the ellipsis:
The components of ‘...’ can be accessed in the usual pairlist manner from C code, but is not easily accessed as an object in interpreted code.
So that's possibly part of the answer to your question: The ellipsis is a not easily accessed object.
You would likely have to go on a safari trip down into the C-code of things to learn more about that.
Now being a not easily accessed object the ... certainly doesn't qualify as a list or vector, which is what pmap_df wants. Although technically it probably could.
However seeing how ~ is used to create one-liner function bodies, the real reason is probably that it itself does not consider the ellipsis in the first place.
Consider:
f <- function( ... ) {
~...
}
f(a=1,b=2) ## returns just `~...` , no trace of a and b in the formula's environment either.
Now consider: ~........ . Works just as fine.
~ happily takes any number of dots or other alfa num characters, ~....... or ~aaa......bb...cc... for example, effectively demonstrating it doesn't care about the otherwise normal way to look at the ellipsis and just treats them as part of a name.

Why the code only works with numbers and not letters?

I have to use the code bellow but I don't completely understand how it works. Why it won't work if I change du.4 by du.f and then use the f when calling the function? For some reason it only works with numbers and I do not undarstand why.
This is the error that it is giving in the case of du.f
Error in paste("Meth1=", nr, ".ps", sep = "") : object 'f' not found
du.4 <- function(u,v,a){(exp(a)*(-1+exp(a*v)))/(-exp(a)+exp(a+a*u)-exp(a*(u+v))+exp(a+a*v))}
plotmeth1 <- function(data1,data2,alpha,nr) {
psfile <-paste("Meth1=",nr,".ps",sep="")
diffmethod <-paste("du.",nr,sep="")
title=paste("Family",nr)
alphavalue <-paste("alpha=",round(alpha,digits=3),sep="")
#message=c("no message")
postscript(psfile)
data3<-sort(eval(call(diffmethod,data1,data2,alpha)))
diffdata <-data3[!is.na(data3)]
#if(length(data3)>length(diffdata))
#{message=paste("Family ",nr,"contains NA!")}
tq <-((1:length(diffdata))/(length(diffdata)+1))
plot(diffdata,tq,main=title,xlab="C1[F(x),G(y)]",ylab="U(0,1)",type="l")
legend(0.6,0.3,c(alphavalue))
abline(0,1)
#dev.off()
}
In R, a dot is used as just another character in identifiers. It is often used for clarity but doesn't have a formal function in defining the part after the dot as being in a name-space given by the part of the identifier before the dot. In something like du.f you can't refer to the function by f alone, even if your computation is inside of an environment named du. You can of course define a function named du.4 and then use 4 all by itself, but when you do so you are using the number 4 as just a number and not as a reference to the function. For example, if
du.4 <- function(u,v,a){(exp(a)*(-1+exp(a*v)))/(-exp(a)+exp(a+a*u)-exp(a*(u+v))+exp(a+a*v))}
Then du.4(1,2,3) evaluates to 21.08554 but attempting to use 4(1,2,3) throws the error
Error: attempt to apply non-function
In the case of your code, you are using paste to assemble the function name as a string to be passed to eval. It makes sense to paste the literal number 4 onto the string 'du.' (since the paste will convert 4 to the string '4') but it doesn't make sense to paste an undefined f onto 'du.'. It does, however, make sense to paste the literal string 'f' onto 'du.', so that the function call plotmeth1 (data1, data2, alpha, 'f') will work even though plotmeth1 (data1, data2, alpha, f) will fail.
See this question for more about the use of the dot in R identifiers.

Turn character strings into named function arguments

I have an R script I intend to call from the command line, which includes a function which may take option ... arguments. I'd like to parse any arguments given at the command line as arguments in .... How might I do this?
I've tried the rlang package. I thought something like this would work:
z <- c('x=1', 'y=2') #but actually, z <- commandArgs(T)
c(UQS(z))
Here I would expect to get a vector as if I had called c(x=1, y=2). Instead I get a list of names x=1 &c.
I agree with the previous answer that this is a bit unsafe. That said, one hacky way to achieve this somewhat safely is to take advantage of environments.
First, create a blank environment:
args_env <- env()
Parse-eval your arguments inside that environment.
z <- c("x=1", "y=2")
eval(parse(text = z), envir = args_env)
Convert the environment to a list:
args_list <- as.list(args_env)
And now, you should be able to use do.call to pass arguments in.
do.call(f, args_list)
I wouldn't necessary recommend your approach. It would be safer to use a package like optparse to properly parse your command line parameters.
But that said, if you want to treat arbitrary strings as R code, you can just eval(parse(test=)) it. Of if you really want to use rlang, you can use eval_tidy/parse_expr.
args <- c('x=1', 'y=2')
z <- eval_tidy(parse_expr(paste0("c(", paste(args, collapse=","), ")")))
# or
z <- eval(parse(text=paste0("c(", paste(args, collapse=","), ")")))
You need this because things in the form a=b can't exist independently outside of an expression otherwise it's interpreted as assignment. If you needed to play with just the name on the left or the value on the right, there might be smarter ways to do this, but if you pass it as one character chunk, you'll need to parse it.

R: IF statement evaluating expression despite condition being FALSE?

I've got a large function in R and the users have the ability to not include/specify an object. If they DO, the code checks to make sure the names in that object match the names in another. If they DON'T, there's no need to do that checking. The code line is:
if(exists("grids")) if(!all(expvarnames %in% names(grids))) {stop("Not all expvar column names found as column names in grids")}
But I'm getting the following error:
Error in match(x, table, nomatch = 0L) : argument "grids" is missing, with no default
Well in this trial run, grids is SUPPOSED to be missing. If I try
if(exists("grids")) print("yay")
Then nothing prints, i.e. the absence of grids means the expression isn't evaluated, which is as I'd expect. So can anyone think why R seems to be evaluating the subsequent IF statement in the main example? Should I slap another set of curly brackets around the second one??
Thanks!
Edit: more problems. Removing "grids," from the functions list of variables means it works if there's no object called grids and you don't specify it in the call (i.e. function(x,grids=whatever)). And keeping "grids," IN the functions list of variables means it works if there IS an object called grids and you do specify it in the call.
Please see this: http://i.imgur.com/9mr1Lwi.png
using exists(grids) is out because exists wants "quotes" and without em everything fails. WITH them ("grids"), I need to decide whether to keep "grids," in the functions list. If I don't, but I specify it in the call (function(x,grids=whatever)) then I get unused argument fail. If I DO, but don't specify it in the call because grids doesn't exist and I don't want to use it, I get match error, grids missing no default.
How do I get around this? Maybe list it in the function variables list as grids="NULL", then rather than if(exists("grids")) do if(grids!="NULL")
I still don't know why the original match problem is happening though. Match is from the expvarnames/grids names checker, which is AFTER if(exists("grids")) which evaluates to FALSE. WAaaaaaaiiiiittttt..... If I specify grids in the function variables list, i.e. simply putting function(x,grids,etc){do stuff}, does that mean the function CREATES an object called grids, within its environment?
Man this is so f'd up....
testfun <- function(x,grids)
{if(exists("grids")) globalgrids<<-grids
print(x+1)}
testfun(1) # Error in testfun(1) : argument "grids" is missing, with no default
testfun <- function(x,grids)
{if(exists("grids")) a<<-c(1,2,3)
print(x+1)}
testfun(1) #2 (and globally assigns a)
So in the first example, the function seems to have created an object called "grids" because exists("grids") evaluates to true. But THEN, ON THE SAME LINE, when asked to do something with grids, it says it doesn't exist! Schroedinger's object?!
This is proven in example 2: grids evaluates true and a is globally assigned then the function does its thing. Madness. Complete madness. Does anyone know WHY this ridiculousness is going on? And is the best solution to use my grids="NULL" default in the functions variables list?
Thanks.
Reproducible example, if you want to but I've already done it for every permutation:
testfun <- function(x,grids)
{if(exists("grids")) if(!all(expvarnames %in% names(grids))) {stop("Not all expvar column names found as column names in grids")}
print(x+1)}
testfun(1)
testfun(x=1,grids=grids)
grids<-data.frame(c(1,2,3),c(1,2,3),c(1,2,3))
expvarnames <- c("a","b","c")
colnames(grids) <- c("a","b","c")
Solution
Adapting your example use:
testfun <- function(x,grids = NULL)
{
if(!is.null(grids)){
if(!all(expvarnames %in% names(grids))){
stop("Not all expvar column names found as column names in grids")
}
print(x+1)
}
}
Using this testfun(1) will return nothing. By specifying a default argument in the function as NULL the function then checks for this (i.e. no argument specified) and then doesn't continue the function if so.
The Reason the Problem Occurs
We go through each of the examples:
testfun <- function(x,grids)
{if(exists("grids")) globalgrids<<-grids
print(x+1)}
testfun(1) # Error in testfun(1) : argument "grids" is missing, with no default
Here we call the function testfun, giving only the x argument. testfun knows it needs two arguments, and so creates local variables x and grids. We have then given an argument to x and so it assigns the value to x. There is no argument to grids, however the variable has still been created, even though no value has been assigned to it. So grids exists, but has no value.
From this exists("grids") will be TRUE, but when we try to do globalgrids<<-grids we will get an error as grids has not been assigned a value, and so we can't assign anything to globalgrids.
testfun <- function(x,grids)
{if(exists("grids")) a<<-c(1,2,3)
print(x+1)}
testfun(1) #2 (and globally assigns a)
This, however is fine. grids exists as in the previous case, and we never actually try and access the value stored in grids, which would cause an error as we have not assigned one.
In the solution, we simply set a default value for grids, which means we can always get something whenever we try and access the variable. Unlike in the previous cases, we will get NULL, not that nothing is stored there.
The main point of this is that when you declare arguments in your function, they are created each time you use the function. They exist. However, if you don't assign them values in your function call then they will exist, but have no value. Then when you try and use them, their lack of values will throw an error.
> a <- c(1,2,3,4)
> b <- c(2,4,6,8)
> if(exists("a")) if(!all(a %in% b)) {stop("Not all a in b")}
Error: Not all a in b
> rm(a)
> if(exists("a")) if(!all(a %in% b)) {stop("Not all a in b")}
>
When a does not exist, the expression does not evaluate, as expected. Before testing your first expression, make sure that grids does not exist by running rm(grids) in the console.
Richard Scriven's comment got me thinking: grids was an argument in my function but was optional, so maybe shouldn't be specified (like anything in "..." optional functions). I commented it out and it worked. Hooray, cheers everyone.

Resources