Pipeable assign function for R -- does not assign anything - r

I have been trying to make a pipeable assign() function for use in loops in conjunction with paste0().
However I cannot get it to actually assign anything, e.g.
assignp <- function(value, x) {
assign(x, value)
}
assignp(13, "thirteen")
print(thirteen)
returns:
Error in print(thirteen) : object 'thirteen' not found
There are no error messages, it just doesn't assign the value to the variable name specified.
Can anyone tell me what I'm doing wrong?

The code in the question does assign it but the assignment is to the environment (sometimes referred to as a frame) that exists within the running function so when that function exits it is lost. Try this definition instead. Note that it is important that envir be an argument to address the general situation.
assignp <- function(value, x, envir = parent.frame()) {
assign(x, value, envir)
}
Below we discuss using it from the global environment with %>%, within a function using %>% and also in both those cases using |> . Also magrittr defines a sequential pipe but not an operator for it and we show how using that can further simplify this. We also show that assignp is not really needed in the first place and we can just use assign.
Using %>%
Note that if we left out .GlobalEnv then thirteen would get injected into a temporary environment created by the pipe so it would not be accessible in the next leg of the pipe and the following would give an error.
library(magrittr)
if (exists("thirteen")) rm(thirteen)
13 %>% assignp("thirteen", .GlobalEnv) %>% { . + thirteen }
## [1] 26
thirteen
## 13
Calling from function using %>%
By passing the current environment thirteen gets defined in it rather than in any temporary environment created by the pipe. We could alternately use e <- .GlobalEnv if we wanted thirteen to be injected into the global environment.
f <- function(x) {
e <- environment()
x %>%
assignp("thirteen", e) %>%
{ . + thirteen }
}
if (exists("thirteen")) rm(thirteen)
f(13)
## [1] 26
exists("thirteen")
## [1] FALSE
Using |>
|> does not create environments so this works.
if (exists("thirteen")) rm(thirteen)
13 |> assignp("thirteen") |> (\(x) x + thirteen)()
## [1] 26
thirteen
## 13
g <- function(x) x |> assignp("thirteen") |> (\(x) x + thirteen)()
if (exists("thirteen")) rm(thirteen)
g(13)
## [1] 26
exists("thirteen")
## [1] FALSE
Using only assign
Actually we don't really need assignp at all. These all work:
if (exists("thirteen")) rm(thirteen)
13 %>% assign("thirteen", ., .GlobalEnv) %>% { . + thirteen }
f2 <- function(x) {
e <- environment()
x %>%
assign("thirteen", ., e) %>%
{ . + thirteen }
}
if (exists("thirteen")) rm(thirteen)
f2(13)
g2 <- function(x) x |> assign(x = "thirteen") |> (\(x) x + thirteen)()
if (exists("thirteen")) rm(thirteen)
g2(13)
magrittr sequential pipe
magrittr defines a sequential pipe but currently there is no operator for it; however, we can readily define one.
`%s>%` <- pipe_eager_lexical
f3 <- function(x) x %s>% assign("thirteen", .) %>% { . + thirteen }
if (exists("thirteen")) rm(thirteen)
f3(13)
## [1] 26
Update
Expanded and fixed errors.

By default assign assigns value in it's current scope i.e within the function in this case. Specify envir = parent.frame() in assign.
assignp <- function(value, x) {
assign(x, value, envir = parent.frame())
#You can also use .GlobalEnv to assign to global environment directly.
#assign(x, value, envir = .GlobalEnv)
}
assignp(13, "thirteen")
thirteen
#[1] 13

Related

R: Using output from one function to set all attributes of another function

I want the output of one function to be able to set all, or possibly only the needed/given, attributes of another. I want to use the output of myFunction1() on its own, which does some calculations and based on that produces multiple needed values, or in combination with myFunction2(), which is supposed to use those values in a plot or similar. The code would look something like this:
myFunction1() >%> myFunction2()
I'm aware that I can possibly put the function that needs the output inside the first function, like:
myFunction1=function(x, logical){
x=x^2
y=""
if(x>100){
y="hello"
}else{
y="goodbye"
}
if(logical){
return(list(x=x,y=y,logical=logical))
}else{
return(myFunction2(x,y,logical))
}
} ##end myFunction1()
myFunction2=function(x, y, z){
a=paste0(x, y, z)
return(a)
}
or use the output with the $-operator
myOutput = myFunction(1, TRUE)
myOutput2 = myFunction2(myOutput$x, myOutput$y, myOutput$logical)
But is there a way to have a list output (or anything that can contain different data types) be able to set all attributes without the need of addressing the output via $ or index?
(First post so feedback regarding the wrongs would be appreciated aswell)
If the question is asking how to create a pipeline from myFunction1 and myFunction2 assuming we can modify myFunction1 but not myFunction2 then remove the test from myFunction1 and put it into the pipeline as shown.
myFunction1 <- function(x, logical) {
y <- if (x^2 > 100) "hello" else "goodbye"
list(x = x, y = y, logical = logical)
}
myFunction2 <- function(x, y, z) {
paste0(x, y, z)
}
# tests - 3 alternatives
library(magrittr)
# 1 - with
myFunction1(1, TRUE) %>%
with(if (logical) myFunction2(x, y, logical) else .)
## [1] "1goodbyeTRUE"
# 2 - magrittr %$%
myFunction1(1, TRUE) %$%
if (logical) myFunction2(x, y, logical) else .
## [1] "1goodbyeTRUE"
# 3 - do.call
myFunction1(1, TRUE) %>%
{ if (.$logical) do.call("myFunction2", unname(.)) else . }
## [1] "1goodbyeTRUE"
If we can modify both we could have myFunction2 accept a list.
myFunction2 <- function(List) {
if (List$logical) do.call("paste0", List) else List
}
myFunction1(1, TRUE) %>% myFunction2
## [1] "1goodbyeTRUE"

Learning how to deal with ... in functions in R

This is my function. Basically I want to include a if else statement inside it but controlling by the length of the arguments that I use in ...:
This is what Ive tried so far, and it is wrong:
soma_mtcars<-function(data,...){
if(length(...) < 2){
sum_df<- data %>% group_by() %>% summarise(total = sum(disp))
}
else(
sum_df<- data %>% group_by() %>% summarise(total = sum(disp))
)
}
Of course the problem is in length(...) < 2. How can I deal with it?
And I would like to have, for example, outputs for: soma_mtcars(mtcars,cyl) and soma_mtcars(mtcars, cyl, disp)
You can use nargs(), which gives you the total number of arguments (i.e. including your data argument):
soma_mtcars <- function (data, ...) {
if (nargs() < 3L) { …
}
… or you can pass ... list, and get the length of its result:
soma_mtcars <- function (data, ...) {
if (length(list(...)) < 2L) { …
}
Either of these will return the length of dot dot dot. The first one does it without evaluating dot dot dot.
len_noeval <- function(...) ...length()
len_eval <- function(...) length(list(...))
# test
len_noeval(11, print(12), 13)
## [1] 3
len_eval(11, print(12), 13)
## [1] 12
## [1] 3

Get name of variable passed as function parameter

I am trying to catch the name of a variable passed to a function, the right way. The name of interest noi is a data frame column or a vector. Below is my minimum working example. Ideally, I would like to receive a character vector which contains only "noi"
library(dplyr)
df <- data.frame(noi = seq(1:3))
example_fun <- function( x ){
deparse(substitute(x))
}
The result depends on the way I structure my input. Now I have an idea why this happens, but how would I do it correctly to have the desired result, regardless of how I call the function.
# Base
example_fun(df$noi)
[1] "df$noi"
# Pipe
df$noi %>% example_fun()
[1] "."
# Mutate
df %>% mutate(example_fun(noi))
noi example_fun(noi)
1 1 noi
2 2 noi
3 3 noi
Thanks in advance!
Perhaps decorate that variable with a "comment" attribute in another function? Note that the variable you want to decorate has to be wrapped directly in the decoration function z; otherwise, an error is raised (by design and for robustness).
example_fun <- function(x){
attr(x, "comment")
}
z <- function(x) {
nm <- substitute(x)
nm <- as.character(
if (is.symbol(nm) && !identical(nm, quote(.))) {
nm
} else if (length(nm) > 1L && (identical(nm[[1L]], quote(`[[`)) || identical(nm[[1L]], quote(`$`)))) {
tail(nm, 1L)
} else {
stop("not a valid symbol or extract operator.", call. = match.call())
}
)
`comment<-`(x, nm)
}
Output
> example_fun(z(df$noi))
[1] "noi"
> z(df$noi) %>% (function(x) x + 1) %>% example_fun()
[1] "noi"
> df %>% mutate(example_fun(z(noi)))
noi example_fun(z(noi))
1 1 noi
2 2 noi
3 3 noi
> z(df[["noi"]]) %>% example_fun()
[1] "noi"
> with(df, z(noi)) %>% example_fun()
[1] "noi"
> z(with(df, noi)) %>% example_fun()
Error in z(with(df, noi)) : not a valid symbol or extract operator.
> df$noi %>% z()
Error in z(.) : not a valid symbol or extract operator.
... but this may not be a robust method. It is extremely difficult to achieve what you want in a robust way, especially when a pipeline is involved. I think you should read Hadley's Advanced R and learn more about how bindings and environments work.

substitute LHS in `=` operator with rlang tidyeval inside Sys.setenv

Problem description
Sys.setenv does not have an easy interface to supply LHS (the env var name) as a parameter. If one wants to dynamically define what env var should be set, then metaprogramming approach is required.
Base R way
This small helper function works as expected.
setenv = function(var, value, quiet=TRUE) {
stopifnot(is.character(var), !is.na(var), length(value)==1L, is.atomic(value))
qc = as.call(c(list(quote(Sys.setenv)), setNames(list(value), var)))
if (!quiet) print(qc)
eval(qc)
}
var_name = "RISCOOL"
Sys.getenv(var_name)
#[1] ""
setenv(var_name, value=150, quiet=FALSE)
#Sys.setenv(RISCOOL = 150)
Sys.getenv(var_name)
#[1] "150"
Question
The question is about how the problem can be solved using packages like pryr or rlang (tidyeval)? or eventually another popular one.
I don't know these packages at all and would like to get better understanding how they could simplify my metaprogramming code.
Note that question is about metaprogramming, setting env var is just an example.
If you want to use rlang-style quasiquotation to construct a call and directly evaluate, you need blast()
blast <- function(expr, env = caller_env()) {
eval_bare(enexpr(expr), env)
}
vars <- c(A = "a", B = "b", C = "c")
blast(data.frame(!!!vars))
#> A B C
#> 1 a b c
In your original example you need to unquote a name. We don't support deep-unquoting on the LHS of := yet (see https://github.com/r-lib/rlang/issues/279), but you can use !!! instead:
setenv <- function(var, value) {
args <- setNames(value, var)
blast(Sys.setenv(!!!args))
}
setenv("foobar", 1)
#> [1] TRUE
Sys.getenv("foobar")
#> [1] "1"
To insert the printed call, blast is too high level but you can use the components:
setenv <- function(var, value, quiet = FALSE) {
args <- setNames(value, var)
call <- expr(Sys.setenv(!!!args))
if (!quiet) {
print(call)
}
# Evaluate in our own environment where `Sys.setenv()` is defined
# (and protected if we're in a package namespace)
eval(call)
}
Use do.call:
var_name = "RISCOOL"
do.call("Sys.setenv", as.list(setNames(3, var_name)))
# check that it worked
Sys.getenv(var_name)
## [1] "3"
or using purrr
library(purrr)
invoke("Sys.setenv", set_names(4, var_name))
I think you need to use :=.
Its usage is explained in one of the dplyr vignettes,
but the functionality is provided by rlang.
In this case you can use call2:
setenv <- function(var, val) {
rlang::call2("Sys.setenv", !!rlang::enexpr(var) := val)
}
setenv(foo, "bar")
# Sys.setenv(foo = "bar")
Just add an eval call as desired.
Just use do.call.
lst <- structure(list(value), names=name)
do.call(Sys.setenv, lst)

How to pass a string as a parameter to a function which expects a variable in R

The first call to the function f works, the second does not. How can I pass a String ("v") to the function f so that the function works as exspected?
library(data.table)
f<-function(t,x) t[,deparse(substitute(x)),with=F]
dat<-data.table(v="a")
f(dat,v)
# v
# 1: a
f(dat,eval(parse(text="v")))
# Error in `[.data.table`(t, , deparse(substitute(x)), with = F) :
# column(s) not found: eval(parse(text = "v"))
It won't be a one-liner anymore but you can test for what you're passing in:
library(data.table)
library(purrr)
dat <- data.table(v="a")
f <- function(dt, x) {
# first, see if 'x' is a variable holding a string with a column name
seval <- safely(eval)
res <- seval(x, dt, parent.frame())
# if it is, then get the value, otherwise substitute() it
if ((!is.null(res$result)) && inherits(res$result, "character")) {
y <- res$result
} else {
y <- substitute(x)
}
# if it's a bare name, then we deparse it, otherwise we turn
# the string into name and then deparse it
if (inherits(y, "name")) {
y <- deparse(y)
} else if (inherits(y, "character")) {
y <- deparse(as.name(x))
}
dt[, y, with=FALSE]
}
f(dat,v)
## v
## 1: a
f(dat, "v")
## v
## 1: a
V <- "v"
f(dat, V)
## v
## 1: a
f(dat, VVV)
#> throws an Error
I switched it from t to dt since I don't like using the names of built-in functions (like t()) as variable names unless I really have to. It can introduce subtle errors in larger code blocks that can be frustrating to debug.
I'd also move the safely() call outside the f() function to save a function call each time you run f(). You can use old-school try() instead, if you like, but you have to check for try-error which may break some day. You could also tryCatch() wrap it, but the safely() way just seems cleaner to me.

Resources