2

I recently discovered that R allows chaining of assignments, e.g.

a = b = 1:10
a
 [1]  1  2  3  4  5  6  7  8  9 10
b
 [1]  1  2  3  4  5  6  7  8  9 10

I then thought that this could also be used in functions, if two arguments should take the same value. However, this was not the case. For example, plot(x = y = 1:10) produces the following error: Error: unexpected '=' in "plot(x = y =". What is different, and why doesn't this work? I am guessing this has something to with only the first being returned to the function, but both seem to be evaluated.

What are some possibilities and constraints with chained assignments in R?

mhovd
  • 3,724
  • 2
  • 21
  • 47
  • 1
    I tried answering it , but I am not sure if you are looking for difference b/w = and <- in this context, as if you replace = with <- , the plot will work. `plot(x <- y<- 1:10)` , Also read it here : https://stackoverflow.com/questions/1741820/what-are-the-differences-between-and-assignment-operators-in-r – PKumar Feb 03 '21 at 12:55
  • 2
    You can do plot(x = x <- 1:10, y = x) but it is really not a good idea to depend on the order that the arguments are evaluated so better would be: x<- 1:10; plot(x = x, y = x) – G. Grothendieck Feb 03 '21 at 13:34
  • @PKumar, that won't produce a plot with x and y being equal to 1:10, but will plot 1:10 against the index. Take `sum(a <- b <- 1:10)` as an example, which will return `55` instead of `110` as expected. – mhovd Feb 03 '21 at 14:22
  • *"What is different, and why doesn't this work?"* Passing arguments to a function is fundamentally different than object assignment. I prefer to use `=` rather than `<-` for assignment--making me a minority in the R community. But the most compelling argument I've hard for using/teaching `<-` is that it makes the difference in usage between passing arguments and doing assignment very clear. – Gregor Thomas Aug 28 '22 at 18:38
  • As to "some possibilities" with chained assignment--generally I would say the possibilities opened up are code that is a few characters shorter at the expense of readability. – Gregor Thomas Aug 28 '22 at 18:39

1 Answers1

9

I don't know about "canonical", but: this is one of the examples that illustrates how assignment (which can be interchangeably be done with <- and =) and passing named arguments (which can only be done using =) are different. It's all about the context in which the expressions x <- y <- 10 or x = y = 10 are evaluated. On their own,

x <- y <- 10
x = y = 10

do exactly the same thing (there are few edge cases where = and <- aren't completely interchangeable as assignment operators, e.g. having to do with operator precedence). Specifically, these are evaluated as (x <- (y <- 10)), or the equivalent with =. y <- 10 assigns the value to 10, and returns the value 10; then x <- 10 is evaluated.

Although it looks similar, this is not the same as the use of = to pass a named argument to a function. As noted by the OP, if f() is a function, f(x = y = 10) is not syntactically correct:

f <- function(x, y) {
  x + y
}
f(x = y = 10)
## Error: unexpected '=' in "f(x = y ="

You might be tempted to say "oh, then I can just use arrows instead of equals signs", but this does something different.

f(x <- y <- 10)
## Error in f(x <- y <- 10) : argument "y" is missing, with no default

This statement tries to first evaluate the x <- y <- 10 expression (as above); once it works, it calls f() with the result. If the function you are calling will work with a single, unnamed argument (as plot() does), and you will get a result — although not the result you expect. In this case, since the function has no default value for y, it throws an error.

People do sometimes use <- with a function call as shortcut; in particular I like to use idioms like if (length(x <- ...) > 0) { <do_stuff> } so I don't have to repeat the ... later. For example:

if (length(L <- list(...))>0) {
  warning(paste("additional arguments to ranef.merMod ignored:",
                paste(names(L),collapse=", ")))
}

Note that the expression length(L <- list(...))>0) could also be written as !length(L <- list(...)) (since the result of length() must be a non-negative integer, and 0 evaluates to FALSE), but I personally think this is a bridge too far in terms of compactness vs readability ... I sometimes think it would be better to forgo the assignment-within-if and write this as L <- list(...); if (length(L)>0) { ... }


PS forcing the association of assignment in the other order leads to some confusing errors, I think due to R's lazy evaluation rules:

rm(x)
rm(y)
## neither x nor y is defined
(x <- y) <- 10
## Error in (x <- y) <- 10 : object 'x' not found
## both x and y are defined
x <- y <- 5
(x <- y) <- 10
## Error in (x <- y) <- 10 : could not find function "(<-"
Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
  • 1
    "It's all about the context in which the expressions x <- y <- 10 or x = y = 10 are evaluated." The main point is that the latter is not parsed as an expression in the context of a function call. – Roland Aug 29 '22 at 05:53
  • 1
    I was using "expression" loosely ... – Ben Bolker Aug 29 '22 at 12:58
  • 1
    when you say: "I like to use idioms like `if (length(x <- ...) > 0) { }` so I don't have to repeat the ... later". Are you just talking about an `else if` situation e.g. `else if (x < 0) {dostuff}`? If not, could you give an example of its benefit, thanks – user63230 Aug 29 '22 at 13:34
  • 1
    Interestingly, even `quote(x = 10)`, `quote(x = y = 10)` and `quote(expr = x = y = 10)` throw an error. One needs to use something like `quote(expr = {x = y = 10})` or `parse(text = "x = y = 10")` to create expressions with `=` used as assignment operator. – Roland Aug 30 '22 at 04:51