Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assignment to replace value in nonlocal list

[[<- 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)
}
like image 339
Konrad Rudolph Avatar asked Jul 17 '15 12:07

Konrad Rudolph


1 Answers

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.
like image 140
Roland Avatar answered Nov 13 '22 09:11

Roland