0

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.

jangorecki
  • 16,384
  • 4
  • 79
  • 160

4 Answers4

3

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)
}
Lionel Henry
  • 6,652
  • 27
  • 33
1

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))
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
  • It might be nicer base R way, but the question is about `rlang` or `pryr` which I would like to understand better. I actually prefer to have an option to print a call before evaluating it, and this is not doable for `do.call`. – jangorecki Dec 06 '19 at 04:14
  • Creating a call and then evaluating it at runtime is not very good programming practice. It means you have to have all of R available at runtime to do the eval. If you must do it with tidyverse use invoke from purrr and set_names from rlang. I have added this to answer. – G. Grothendieck Dec 06 '19 at 11:50
  • "you have to have all of R available at runtime to do the eval" - I don't understand this. It doesn't have to be tidyverse way, but some popular package for metaprogramming, to see how it can help in tasks like this. purrr example is another one nice method for that. Thanks – jangorecki Dec 06 '19 at 12:49
  • If the problem requires `eval` then of course use it but if you can avoid it you should. – G. Grothendieck Dec 06 '19 at 12:53
  • eval is invoked anyway by `do.call` just not explicitly with `eval` function, isn't it? – jangorecki Dec 06 '19 at 12:57
  • but I don't get why that difference matters? – jangorecki Dec 06 '19 at 13:40
  • Imagine that R were compiled. Then `do.call` would only need a runtime that included the function invoked by `do.call` but `eval` would need all of R. – G. Grothendieck Dec 06 '19 at 13:47
1

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.

Alexis
  • 4,950
  • 1
  • 18
  • 37
0

Just use do.call.

lst <- structure(list(value), names=name)
do.call(Sys.setenv, lst)
Hong Ooi
  • 56,353
  • 13
  • 134
  • 187
  • It might be nicer base R way, but the question is about `rlang`or `pryr` which I would like to understand better. I actually prefer to have an option to print a call before evaluating it, and this is not doable for `do.call`. – jangorecki Dec 06 '19 at 04:16