0

I am writing a R equivalent to Pythons 'pop' method. I know 99th percentile has one but I'd prefer my own (practice/understanding/consistency etc). For reference, pop() takes an object and removes the first item from the object whilt also returning it. So

> l <- c(1,3,5)
> x <- pop(l)
> print(l)
> 3, 5
> print(x)
> 1

I am using assign() to replace the input object with one less the first value and returning said first value from the function.

My question is, how do I get the environment of the input object and use this environment within assign()? I have tried using pryr::where() which returns 'R_GlobalEnv' but I can't use this value in assign(). Instead the only value I can get to work in assign() is 'globalenv()'.

Posted from mobile so let me know if something doesn't work.

Ben
  • 89
  • 7
  • Are you looking for something like this [R-Help post](https://stat.ethz.ch/pipermail/r-help/2003-February/030301.html)? – Rui Barradas Aug 15 '20 at 16:23

3 Answers3

4

You can implement this in base R, though it's not advised. R is a functional language and functions with side effects are not expected by end-users.

pop <- function(vec)
{
  vec_name <- deparse(substitute(vec))
  assign(vec_name, vec[-1], envir = parent.frame())
  vec[1]
}

a <- c(2, 7, 9)

a
#> [1] 2 7 9

pop(a)
#> [1] 2

a
#> [1] 7 9

pop(a)
#> [1] 7

a
#> [1] 9

Created on 2020-08-15 by the reprex package (v0.3.0)

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • I've just edited my answer: I'd be interested if you see a way to handle the indirect pop I describe there. – user2554330 Aug 15 '20 at 19:17
  • Marked as correct because it gets the env I was after. You are correct, this isn't what a user would expect from **functional** R, this however is being used in a program which is has a R6 OOP framework. A message is added letting the user know the original object has been changed. Thanks! – Ben Aug 24 '20 at 15:58
2

You can do it using pryr::promise_info(l)$env, but it's a very un-R-like thing to do. Functions shouldn't have side effects.

For example,

    pop <- function(l) {
      info <- pryr::promise_info(l)
      if (!is.name(info$code))
        stop("Argument expression should be a name.")
      result <- l[[1]]  # work on lists too
      assign(as.character(info$code), l[-1], envir = info$env)
      result
    }
    l <- c(1, 3, 5)
    pop(l)
#> Registered S3 method overwritten by 'pryr':
#>   method      from
#>   print.bytes Rcpp
#> [1] 1
    l
#> [1] 3 5

Created on 2020-08-15 by the reprex package (v0.3.0)

Edited to add: Interestingly, none of the three answers so far works in complicated situations like this one:

f <- function(x) {
  cat("The pop(x) result is", pop(x), "\n")
  cat("Now x is ", x, "\n")
  cat("Now l is ", l, "\n")
}

l <- c(1, 3, 5)
f(l)

@RuiBarradas's answer gives

The pop(x) result is 5 
Now x is  1 3 5 
Now l is  1 3 5 

(He pops the last value rather than the first which is not a big deal, but neither x nor l is modified.)

@AllanCameron's answer gives

The pop(x) result is 1 
Now x is  3 5 
Now l is  1 3 5 

This is arguably correct (x got popped), but I think it would be nice to have l being popped, and that seems tricky.

My answer dies with this message:

Error in pop(x) : Argument expression should be a name.

which seems like a bug: obviously whether it's getting x or l, it really is a name. The problem seems to be in pryr::promise_info, which returns the compiled code that would return the value of x, rather than just the code for x. If I turn off JIT compiling by compiler::enableJIT(0), I get the same result as @AllanCameron. It's not clear to me how to unwind back the right amount to pop l instead of just x.

user2554330
  • 37,248
  • 4
  • 43
  • 90
  • I don't think I'd be too happy about function `f` popping the original `l`. What if we wanted a function that used `pop` as part of some algorithm to process a vector to generate a new result from it? We would wreck our initial vector. We couldn't pass a data frame column to it, for example. That's why I wouldn't use `pop`, and if I did, it would only be for internal use and wouldn't overwrite anything higher than the first parent frame. So I think the behaviour you show is desired and correct. It's a feature, not a bug ;) – Allan Cameron Aug 15 '20 at 19:31
  • Obviously the name `f` isn't very informative, but what if it was a function called `showThenPop`, that printed its argument before popping? In that case you'd want the pop to apply to `l`, but you couldn't do it with our implementations of the `pop` function, you'd need to copy the body of `pop` into `showThenPop`. Basically `pop` is all about passing by reference, and it seems hard to fake passing by reference two levels down. – user2554330 Aug 15 '20 at 20:06
  • If you really wanted to do that, you could just use the same trick in the outer function of rewriting `x` to `l` in the parent frame. This again allows you to limit your actions to the parent frame. There just isn't a need to make it any more complicated. – Allan Cameron Aug 15 '20 at 20:26
  • This is really useful for anyone else coming to this, I've marked another answer as correct because it provided a solution to my specific situation, but as mentioned, its not for functional use. Having this here so others can see the problem is really useful, thanks. – Ben Aug 24 '20 at 15:57
2

The following answer is based in this R-Help post, function pop with function getEnvOf from this SO post, both adapted to the question's problem.

getEnvOf <- function(what, which=rev(sys.parents())) {
  what <- as.character(substitute(what))
  for (frame in which)
    if (exists(what, frame=frame, inherits=FALSE))
      return(sys.frame(frame))
  return(NULL)
}
pop <- function(x){
  y <- as.character(substitute(x))
  e <- getEnvOf(y)
  if(length(x) > 0) {
    val <- x[[length(x)]]
    assign(y, x[-length(x)], envir = parent.env(e))
    val
  } else {
    msg <- paste(sQuote(y), "length is not > 0")
    warning(msg)
    NULL
  }
}

y <- c(1,3,5)
pop(y)

This also works with lists.

z <- list(1, 2, 5)
pop(z)

w <- list(1, c(2, 4, 6), 5)
pop(w)
#[1] 5

pop(w)
#[1] 2 4 6

pop(w)
#[1] 1

pop(w)
#NULL
#Warning message:
#In pop(w) : ‘w’ length is not > 0
Rui Barradas
  • 70,273
  • 8
  • 34
  • 66