Assign multiple new variables on LHS in a single line
Asked Answered
T

15

117

I want to assign multiple variables in a single line in R. Is it possible to do something like this?

values # initialize some vector of values
(a, b) = values[c(2,4)] # assign a and b to values at 2 and 4 indices of 'values'

Typically I want to assign about 5-6 variables in a single line, instead of having multiple lines. Is there an alternative?

Toritorie answered 22/9, 2011 at 18:44 Comment(7)
you mean something like in PHP list($a, $b) = array(1, 2)? That would be nice! +1.Humfrey
@Humfrey T - I think my vassign suggestion below comes close... :)Composite
Note: Semicolons aren't needed for this bit of R.Paulie
If you'd try this within an appropriate environment, that would be as easy as X <- list();X[c('a','b')] <- values[c(2,4)]. OK, you don't assign them in the workspace, but keep them nicely together in a list. I'd prefer to do it that way.Diagenesis
i like python, just a, b = 1,2. all the answers below are 100x harderDrawbar
@Drawbar It is called "destructuring assignment" and most languages (even some not considered "scripting languages") support it these days.Apples
I think the OP (if they are still around?) or the community account should accept an answer. That way we can legitimately flag dupes (as it seems this question has popular, high quality answers)Pimp
P
52

I put together an R package zeallot to tackle this very problem. zeallot includes an operator (%<-%) for unpacking, multiple, and destructuring assignment. The LHS of the assignment expression is built using calls to c(). The RHS of the assignment expression may be any expression which returns or is a vector, list, nested list, data frame, character string, date object, or custom objects (assuming there is a destructure implementation).

Here is the initial question reworked using zeallot (latest version, 0.0.5).

library(zeallot)

values <- c(1, 2, 3, 4)     # initialize a vector of values
c(a, b) %<-% values[c(2, 4)]  # assign `a` and `b`
a
#[1] 2
b
#[1] 4

For more examples and information one can check out the package vignette.

Photoelectron answered 26/7, 2017 at 14:26 Comment(2)
this is exactly what I was hoping to find, something that enables the python-like syntax the OP was asking for, implemented in an R packageWoodbury
What about assigning a matrix to each variable name?Greenheart
K
47

There is a great answer on the Struggling Through Problems Blog

This is taken from there, with very minor modifications.

USING THE FOLLOWING THREE FUNCTIONS (Plus one for allowing for lists of different sizes)

# Generic form
'%=%' = function(l, r, ...) UseMethod('%=%')

# Binary Operator
'%=%.lbunch' = function(l, r, ...) {
  Envir = as.environment(-1)

  if (length(r) > length(l))
    warning("RHS has more args than LHS. Only first", length(l), "used.")

  if (length(l) > length(r))  {
    warning("LHS has more args than RHS. RHS will be repeated.")
    r <- extendToMatch(r, l)
  }

  for (II in 1:length(l)) {
    do.call('<-', list(l[[II]], r[[II]]), envir=Envir)
  }
}

# Used if LHS is larger than RHS
extendToMatch <- function(source, destin) {
  s <- length(source)
  d <- length(destin)

  # Assume that destin is a length when it is a single number and source is not
  if(d==1 && s>1 && !is.null(as.numeric(destin)))
    d <- destin

  dif <- d - s
  if (dif > 0) {
    source <- rep(source, ceiling(d/s))[1:d]
  }
  return (source)
}

# Grouping the left hand side
g = function(...) {
  List = as.list(substitute(list(...)))[-1L]
  class(List) = 'lbunch'
  return(List)
}


Then to execute:

Group the left hand side using the new function g() The right hand side should be a vector or a list Use the newly-created binary operator %=%

# Example Call;  Note the use of g()  AND  `%=%`
#     Right-hand side can be a list or vector
g(a, b, c)  %=%  list("hello", 123, list("apples, oranges"))

g(d, e, f) %=%  101:103

# Results: 
> a
[1] "hello"
> b
[1] 123
> c
[[1]]
[1] "apples, oranges"

> d
[1] 101
> e
[1] 102
> f
[1] 103


Example using lists of different sizes:

Longer Left Hand Side

g(x, y, z) %=% list("first", "second")
#   Warning message:
#   In `%=%.lbunch`(g(x, y, z), list("first", "second")) :
#     LHS has more args than RHS. RHS will be repeated.
> x
[1] "first"
> y
[1] "second"
> z
[1] "first"

Longer Right Hand Side

g(j, k) %=% list("first", "second", "third")
#   Warning message:
#   In `%=%.lbunch`(g(j, k), list("first", "second", "third")) :
#     RHS has more args than LHS. Only first2used.
> j
[1] "first"
> k
[1] "second"
Khalil answered 12/11, 2012 at 23:54 Comment(0)
R
42

Consider using functionality included in base R.

For instance, create a 1 row dataframe (say V) and initialize your variables in it. Now you can assign to multiple variables at once V[,c("a", "b")] <- values[c(2, 4)], call each one by name (V$a), or use many of them at the same time (values[c(5, 6)] <- V[,c("a", "b")]).

If you get lazy and don't want to go around calling variables from the dataframe, you could attach(V) (though I personally don't ever do it).

# Initialize values
values <- 1:100

# V for variables
V <- data.frame(a=NA, b=NA, c=NA, d=NA, e=NA)

# Assign elements from a vector
V[, c("a", "b", "e")] = values[c(2,4, 8)]

# Also other class
V[, "d"] <- "R"

# Use your variables
V$a
V$b
V$c  # OOps, NA
V$d
V$e
Remonstrate answered 25/2, 2013 at 14:2 Comment(4)
+10 if I could. I wonder why people refuse to use lists in such obvious cases, but rather litter the workspace with tons of meaningless variables. (you do use lists, as a data.frame is a special kind of list. I'd just use a more general one.)Diagenesis
but you can't have different kind of elements in the same column, nor can you store dataframes or lists inside your dataframeSemantic
Actually, you can store lists in a data frame - google "list column".Houchens
It's not a bad approach, it has some conveniences, but it's also not that hard to imagine why many users would not want to have to deal with data.frame syntax every time they were trying to use or access variables assigned in this way.Jersey
E
15

here is my idea. Probably the syntax is quite simple:

`%tin%` <- function(x, y) {
    mapply(assign, as.character(substitute(x)[-1]), y,
      MoreArgs = list(envir = parent.frame()))
    invisible()
}

c(a, b) %tin% c(1, 2)

gives like this:

> a
Error: object 'a' not found
> b
Error: object 'b' not found
> c(a, b) %tin% c(1, 2)
> a
[1] 1
> b
[1] 2

this is not well tested though.

Ermentrude answered 23/9, 2011 at 1:56 Comment(1)
Koshke, looks very nice to me :-) But I'm a bit worried about operator precedence: the %something% operators are pretty high up, so the behaviour of e.g. c(c, d) %tin% c(1, 2) + 3 (=> c = 1, d = 1, returns numeric (0)) may be considered surprising.Shylashylock
A
11

A potentially dangerous (in as much as using assign is risky) option would be to Vectorize assign:

assignVec <- Vectorize("assign",c("x","value"))
#.GlobalEnv is probably not what one wants in general; see below.
assignVec(c('a','b'),c(0,4),envir = .GlobalEnv)
a b 
0 4 
> b
[1] 4
> a
[1] 0

Or I suppose you could vectorize it yourself manually with your own function using mapply that maybe uses a sensible default for the envir argument. For instance, Vectorize will return a function with the same environment properties of assign, which in this case is namespace:base, or you could just set envir = parent.env(environment(assignVec)).

Alejandrinaalejandro answered 22/9, 2011 at 19:15 Comment(0)
U
10
list2env(setNames(as.list(rep(2,5)), letters[1:5]), .GlobalEnv)

Served my purpose, i.e., assigning five 2s into first five letters.

Ulcer answered 17/4, 2016 at 13:51 Comment(0)
C
9

As others explained, there doesn't seem to be anything built in. ...but you could design a vassign function as follows:

vassign <- function(..., values, envir=parent.frame()) {
  vars <- as.character(substitute(...()))
  values <- rep(values, length.out=length(vars))
  for(i in seq_along(vars)) {
    assign(vars[[i]], values[[i]], envir)
  }
}

# Then test it
vals <- 11:14
vassign(aa,bb,cc,dd, values=vals)
cc # 13

One thing to consider though is how to handle the cases where you e.g. specify 3 variables and 5 values or the other way around. Here I simply repeat (or truncate) the values to be of the same length as the variables. Maybe a warning would be prudent. But it allows the following:

vassign(aa,bb,cc,dd, values=0)
cc # 0
Composite answered 22/9, 2011 at 22:8 Comment(5)
I like this, but I would worry that it might break in some case where it was called from within a function (although a simple test of this worked, to my mild surprise). Can you explain ...(), which looks like black magic to me ... ?Natica
@Ben Bolker - Yes, ...() is extreme black magic ;-). It so happens that when the "function call" ...() gets substituted, it becomes a pairlist which can be passed to as.character and voila, you got the arguments as strings...Composite
@Ben Bolker - And it should work correctly even when called from within a function since it uses envir=parent.frame() - And you can specify e.g. envir=globalenv() if you want.Composite
Even cooler would be having this as replacement function: `vassign<-` <- function (..., envir = parent.frame (), value) and so on. However, it seems that the first object to be assigned would need to exist already. Any ideas?Shylashylock
@cbeleites - Yes, that would be cooler but I don't think you can work around the limitation that the first argument has to exist - that's why it's called a replacement function :) ...but let me know if you find out otherwise!Composite
A
7

Had a similar problem recently and here was my try using purrr::walk2

purrr::walk2(letters,1:26,assign,envir =parent.frame()) 
Autobiographical answered 28/3, 2018 at 20:13 Comment(0)
G
5

https://stat.ethz.ch/R-manual/R-devel/library/base/html/list2env.html:

list2env(
        list(
            a=1,
            b=2:4,
            c=rpois(10,10),
            d=gl(3,4,LETTERS[9:11])
            ),
        envir=.GlobalEnv
        )
Gainey answered 16/9, 2015 at 22:53 Comment(0)
P
3

If your only requirement is to have a single line of code, then how about:

> a<-values[2]; b<-values[4]
Phallus answered 22/9, 2011 at 18:59 Comment(2)
was looking for a succinct statement but I guess there is noneToritorie
I'm on the same boat as @user236215. when the right hand side is a complicated expression returning a vector, repeating code seems very wrong...Pathological
M
2

For a named list, use

list2env(mylist, environment())

For instance:

mylist <- list(foo = 1, bar = 2)
list2env(mylist, environment())

will add foo = 1, bar = 2 to the current environement, and override any object with those names. This is equivalent to

mylist <- list(foo = 1, bar = 2)
foo <- mylist$foo
bar <- mylist$bar

This works in a function, too:

f <- function(mylist) {
  list2env(mylist, environment())
  foo * bar
}
mylist <- list(foo = 1, bar = 2) 
f(mylist)

However, it is good practice to name the elements you want to include in the current environment, lest you override another object... and so write preferrably

list2env(mylist[c("foo", "bar")], environment())

Finally, if you want different names for the new imported objects, write:

list2env(`names<-`(mylist[c"foo", "bar"]), c("foo2", "bar2")), environment())

which is equivalent to

foo2 <- mylist$foo
bar2 <- mylist$bar
Moravian answered 31/7, 2022 at 9:31 Comment(0)
H
1

I'm afraid that elegent solution you are looking for (like c(a, b) = c(2, 4)) unfortunatelly does not exist. But don't give up, I'm not sure! The nearest solution I can think of is this one:

attach(data.frame(a = 2, b = 4))

or if you are bothered with warnings, switch them off:

attach(data.frame(a = 2, b = 4), warn = F)

But I suppose you're not satisfied with this solution, I wouldn't be either...

Humfrey answered 22/9, 2011 at 20:53 Comment(0)
F
1
R> values = c(1,2,3,4)
R> a <- values[2]; b <- values[3]; c <- values[4]
R> a
[1] 2
R> b
[1] 3
R> c
[1] 4
Fiance answered 29/11, 2013 at 2:20 Comment(0)
A
1

Combining some of the answers given here + a little bit of salt, how about this solution:

assignVec <- Vectorize("assign", c("x", "value"))
`%<<-%` <- function(x, value) invisible(assignVec(x, value, envir = .GlobalEnv))

c("a", "b") %<<-% c(2, 4)
a
## [1] 2
b
## [1] 4

I used this to add the R section here: http://rosettacode.org/wiki/Sort_three_variables#R

Caveat: It only works for assigning global variables (like <<-). If there is a better, more general solution, pls. tell me in the comments.

Anthropogenesis answered 31/12, 2018 at 23:41 Comment(0)
L
0

Another version with recursion:

let <- function(..., env = parent.frame()) {
    f <- function(x, ..., i = 1) {
        if(is.null(substitute(...))){
            if(length(x) == 1)
                x <- rep(x, i - 1);
            stopifnot(length(x) == i - 1)
            return(x);
        }
        val <- f(..., i = i + 1);
        assign(deparse(substitute(x)), val[[i]], env = env);
        return(val)
    }
    f(...)
}

example:

> let(a, b, 4:10)
[1]  4  5  6  7  8  9 10
> a
[1] 4
> b
[1] 5
> let(c, d, e, f, c(4, 3, 2, 1))
[1] 4 3 2 1
> c
[1] 4
> f
[1] 1

My version:

let <- function(x, value) {
    mapply(
        assign,
        as.character(substitute(x)[-1]),
        value,
        MoreArgs = list(envir = parent.frame()))
    invisible()
}

example:

> let(c(x, y), 1:2 + 3)
> x
[1] 4
> y
[1] 
Lixiviate answered 30/3, 2017 at 12:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.