Malgorithm
Malgorithm

Reputation: 21

R - Function that accesses a variable defined in another function on the same level

So I'm in the middle of a project at the moment, and I've encountered a problem with scoping. Below is a simplification of my problem.

a <- function() {
  x <- 1:3    ## x is defined in a()
  w <- d()
  x
  w
}

b <- function() {
  y <- x[2]   ## I want to access x from b()
  x <- x[-2]  ## and modify x
  return(y)
}

d <- function() {
  z <- b()    ## then call b() from d()
  return(list(1, z, 3, 4))
}
a()

When I run a(), the ideal output would be w equal to list(1, 2, 3, 4) and x equal to c(1, 3) . I am aware that I could define x globally, however x should be 'invisible' to d(). I have tried using parent.frame() in b(), but it doesn't work, probably because d() is its parent and not a(). I have also messed around with environment(), but I'm having trouble understanding the structure.

Upvotes: 1

Views: 71

Answers (1)

G. Grothendieck
G. Grothendieck

Reputation: 269824

1) local environment We can define the functions in a local environment which shares x. Since x, b and d are only used internally we only need to provide a back to the workspace.

a <- local({
  x <- NULL
  b <- function() { y <- x[2]; x <<- x[-2]; y }  
  d <- function() { z <- b(); list(1, z, 3, 4) }
  function() { x <<- 1:3; d() }
})

giving:

> x  # x should not be visible
Error: object 'x' not found

> a()
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

2) nested functions Another approach is to nest the functions:

a <- function() {
  b <- function() { y <- x[2]; x <<- x[-2]; y }  
  d <- function() { z <- b(); list(1, z, 3, 4) }
  x <- 1:3
  d() 
}
a()

giving:

[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

3) Using object models

Although this allows us to run a without having x visible it may be preferable to use one of the R object models to define an object in which a, b and d are methods of that object and x is a property.

The proto package implements a pure object based model (i.e. no classes) which seems particularly approrpiate here:

library(proto)
p <- proto(x = NULL,
     b = function(.) { y <- .$x[2]; .$x <- .$x[-2]; y},
     d = function(.) { z <- .$b(); list(1, z, 3, 4) },
     a = function(.) { .$x <- 1:3; .$d() }
)
p$a()

giving:

[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

The proto code can also be written in the style of (1) and (2) but then inheritance won't work. Since we don't use that here anyways it may not matter.

library(proto)
p <- proto(x = NULL,
     b = function(.) { y <- x[2]; x <<- x[-2]; y},
     d = function(.) { z <- b(); list(1, z, 3, 4) },
     a = function(.) { x <<- 1:3; d() }
)
p$a()

4) Making x invisible to d If you really want to make x not visible to d then for (1) change it to this:

L <- local({
  x <- NULL
  list(b = function() { y <- x[2]; x <<- x[-2]; y },
       a = function() { x <<- 1:3; d() })
})
d <- function() { z <- L$b(); list(1, z, 3, 4) }
L$a()

Actually if d really wanted to get to x it could access it like this; however, one would really have to make an effort:

environment(a)$x

For (2) we would change the code to this:

L <- function() {
  x <- NULL
  list(b = function() { y <- x[2]; x <<- x[-2]; y },
       a = function() { x <<- 1:3; d() })
}
L <- L()
d <- function() { z <- L$b(); list(1, z, 3, 4) }
L$a()

Again it is actually possible for d to access x but, again, it requires considerable effort:

environment(L$a)$x

For (3) we change the code to this:

library(proto)
p <- proto(.x = NULL,
     b = function(.) { y <- .$.x[2]; .$x <- .$.x[-2]; y},
     a = function(.) { .$.x <- 1:3; d() }
)
d = function() { z <- p$b(); list(1, z, 3, 4) }
p$a()

As with (1) and (2) with some effort we can access x from d if we really want via:

p$.x

but accessing a dot variable from outside its object would be quite noticeable.

Note: Based on comment you might prefer this:

e <- local({ 
     self <- environment()
     x <- NULL
     a <-  function() { self$x <- 1:3; d(self) }
     b <- function() { y <- x[2]; self$x <- x[-2]; y }  
     self
})
d <- function(e) { z <- e$b(); list(1, z, 3, 4) }
e$a()

This makes it clear that e is identified as an object of which a and b are methods and x is a property and d is acting on the passed environment. It also avoids the ugly environment(a)$b() referred to in the comment.

Although less preferred, you could omit the argument to d and just hard code e into d. Note that d can access x via e$x so it is not truly invisible but one would have to go to the extra effort of qualifying it. The reason this is less preferred is that by hard-coding e into d we are tying d to e. In that case it would be more logical to just put d right in e since it is tied to anyways. On the other hand, if we pass e to d then d is not tied to any particular environment.

Upvotes: 1

Related Questions