17

[[<- behaves differently for lists and environments when used on non-local objects:

lst = list()
env = new.env()

(function () lst[['x']] = 1)()
(function () env[['x']] = 1)()
lst
# list()

as.list(env)
# $x
# [1] 1

In other words, if the target of [[<- is an environment, it modifies the (nonlocal) environment, but if it’s a vector/list, it creates a new, local object.

I would like to know two things:

  1. Why this difference in behaviour?
  2. Is there a way of achieving the same result for lists as for environments, without using <<-?

Regarding (1), I’m aware of multiple differences between lists and environments (in particular, I know that environments don’t get copied) but the documentation does not mention why the semantics of [[<- differ between the two — in particular, why the operator would operate on different scope. Is this a bug? It’s counter-intuitive at least, and requires some non-trivial implementation shenanigans.1

Regarding (2), the obvious solution is of course to use <<-:

(function () lst[['x']] <<- 1)()

However, I prefer understanding the difference rigorously rather than just working around them. Furthermore, I’ve so far used assign instead of <<- and I prefer this, as it allows me greater control over the scope of the assignment (in particular since I can specify inherits = FALSE. <<- is too much voodoo for my taste.

However, the above cannot be solved (as far as I know) using assign because assign only works on environments, not lists. In particular, while assign('x', 1, env) works (and does the same as above), assign('x', 1, lst) doesn’t work.


1 To elaborate, it’s of course expected that R does different thing for different object types using dynamic dispatch (e.g. via S3). However, this is not the case here (at least not directly): the distinction in scope resolution happens before the object type of the assignment target is known — otherwise the above would operate on the global lst, rather than creating a new local object. So internally [[<- has to do the equivalent of:

`[[<-` = function (x, i, value) {
    if (exists(x, mode = 'environment', inherits = TRUE))
        assign(i, value, pos = x, inherits = FALSE)
    else if (exists(x, inherits = FALSE)
        internal_assign(x, i, value)
    else
        assign(x, list(i = value), pos = parent.frame(), inherits = FALSE)
}
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 1
    This is a great question, but I just wanted to point out that the above behavior is equally true for `$<-` (as you would probably expect) in addition to `[[<-`, most likely due to the reference semantics inherent to `environments`, as covered by @Roland below. – nrussell Jul 17 '15 at 13:49
  • 1
    @nrussell Yes, I contemplated mentioning `$` but according to the documentation, its semantics are anyway defined as equivalent to `[[i, exact = FALSE]]`, and the same for assignment. – Konrad Rudolph Jul 17 '15 at 13:50

1 Answers1

8

The R-language definition (section 2.1.10) says:

Unlike most other R objects, environments are not copied when passed to functions or used in assignments.

Section "6.3 More on evaluation" also gives a slightly relevant hint:

Notice that evaluation in a given environment may actually change that environment, most obviously in cases involving the assignment operator, such as

eval(quote(total <- 0), environment(robert$balance)) # rob Rob

This is also true when evaluating in lists, but the original list does not change because one is really working on a copy.

So, the answer to your first question is that lists need to be copied to assign into them, but environments can be modified in place (which has huge performance implications).

Regarding your second question:

If you are working with a list, the only option seems to be to

  • copy the list into the local scope (using get),
  • assign into the list,
  • use assign to copy the modified list back into the original environment.
Roland
  • 127,288
  • 10
  • 191
  • 288
  • So, just to understand, what happens internally is that (1) the object gets passed to `[[<-` as an argument, which creates a copy (which, in the case of environments, does not perform a copy, as it never does). So far, this is just a vanilla function call and I understand that. (2) And then some “R magic” causes the result of the `[[<-` call to be assigned to a local object of the same name as its argument (which is presumably the same regardless of object type, and happens for all assignment operators). This makes some sense. – Konrad Rudolph Jul 17 '15 at 13:55
  • Incidentally, here’s an alternative solution to my second question, inspired by the passage you quoted: `eval.parent(bquote({lst[['x']] = .(value)}))`. Don’t worry, I won’t use that. :p – Konrad Rudolph Jul 17 '15 at 13:57
  • 1
    In case of lists, you can think of `[[<-` as a function with input values (which are searched for following the usual scoping rules) and the modified list as a return value, which is lost because you don't return it from your function. – Roland Jul 17 '15 at 14:01
  • 1
    But note that `[[<-` actually returns the assigned value invisibly: `res <- (function () lst[['x']] = 1)(); res` So, what really happens is that the copied list is assigned into the local environment and lost when the function is exited. – Roland Jul 17 '15 at 14:03
  • 1
    @Roland Your comment says “in case of lists, …” — but in fact (and that’s what makes this whole behaviour consistent) it’s not just in case of lists, it’s for every type of object, including environments. We can verify this by modifying my original function ever so slightly: `(function () {env[['x']] = 1; get('env', inherits = FALSE)})()` — This will return the *local* copy of `env`, which was created by `[[<-` (but which obviously is a reference to the original `env`). This is reassuring, because if `[[<-` with environments didn’t create this local copy, nothing here would make sense. – Konrad Rudolph Jul 17 '15 at 16:56