Assign multiple new variables on LHS in a single line

67,762

Solution 1

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"

Solution 2

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.

Solution 3

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

Solution 4

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.

Solution 5

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)).

Share:
67,762
user236215
Author by

user236215

Updated on January 02, 2020

Comments

  • user236215
    user236215 over 4 years

    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?

  • user236215
    user236215 over 12 years
    was looking for a succinct statement but I guess there is none
  • Ben Bolker
    Ben Bolker over 12 years
    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 ... ?
  • Tommy
    Tommy over 12 years
    @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...
  • Tommy
    Tommy over 12 years
    @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.
  • cbeleites unhappy with SX
    cbeleites unhappy with SX over 12 years
    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?
  • cbeleites unhappy with SX
    cbeleites unhappy with SX over 12 years
    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.
  • Tommy
    Tommy over 12 years
    @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!
  • Joris Meys
    Joris Meys about 11 years
    +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.)
  • airstrike
    airstrike almost 8 years
    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...
  • skan
    skan almost 7 years
    but you can't have different kind of elements in the same column, nor can you store dataframes or lists inside your dataframe
  • Admin
    Admin over 6 years
    Actually, you can store lists in a data frame - google "list column".
  • jafelds
    jafelds over 4 years
    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 package
  • Brandon
    Brandon over 4 years
    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.
  • StatsSorceress
    StatsSorceress about 4 years
    What about assigning a matrix to each variable name?