Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I detect changes to R's global environment during interactive execution?

Tags:

r

I want to write a background program that detects changes to R's global environment and alerts the user if any variable is being overwritten (during an interactive session).

I've tinkered with the following code:

environment_changed <- function(before_env, after_env) {
  !identical(before_env, after_env)
}

check_environment_change <- function(before_env) {
  after_env <- as.list(.GlobalEnv)
  if (has_environment_changed(before_env, after_env)) {
    print("Change detected!")
    }
  }

addTaskCallback(check_environment_change)

x <- 1
x <- 2 # doesn't work

It doesn't work. I do not really understand how addTaskCallback works, but I thought this might be one possible approach. How else might this be achieved?

like image 520
David Ranzolin Avatar asked Sep 01 '25 01:09

David Ranzolin


1 Answers

Using the example in addTaskCallback() as a template, I made the following. It uses the times() argument to identify how many times the additional callback will run (which I have currently set to Inf, so you'll have to shut it off with removeTaskCallback(). What it currently does is check the environment for names that are the same before and after the execution of the task. Then, it returns a message if any of those objects have changed. Here's an example:

times <- function(total = Inf, before_env = as.list(.GlobalEnv)) {
  ctr <- 0
  function(expr, value, ok, visible) {
    check_environment_overwrite <- function(before_env) {
      after_env <- as.list(.GlobalEnv)
      olaps <- intersect(names(before_env), names(after_env))
      if(length(olaps) > 0){
        if(!identical(before_env[olaps], after_env[olaps])){
          out <- lapply(seq_along(olaps), \(i){
            identical(before_env[[olaps[i]]], 
                      after_env[[olaps[i]]])})
          outv <- do.call("c", out)
          names(outv) <- olaps
            cat("The following elements have changed in .GlobalEnv: ", 
                names(outv)[which(!outv)], "\n", sep="")
          }
      }
    }
    check_environment_overwrite(before_env)
    before_env <<- as.list(.GlobalEnv)
    ctr <<- ctr + 1
    keep.me <- (ctr < total)
    if (!keep.me)
      cat("handler removing itself\n")
    
    # return
    keep.me
  }
}

To get the same result, start with a workspace that doesn't include x or z:

# > n <- addTaskCallback(times())
# > x <- 1
# > z <- 1
# > x <- 2
# The following elements have changed in .GlobalEnv: x
# > z <- 1
# > z <- 2
# The following elements have changed in .GlobalEnv: z
# > 
# > removeTaskCallback(n)
# [1] TRUE

Edit: alternative assignment operator that checks overwriting beforehand

In the spirit of the question, the function above will notify you after you overwrote something. We could make an alternative assignment operator that looks to see whether something will be overwritten and if it will be, it asks the user if they want to overwrite it.

`%a%` <- function(lhs, rhs){
  lhs_name <- as.character(substitute(lhs))
  if(lhs_name %in% names(.GlobalEnv)){
   rpl <- askYesNo(paste0("Do you want to replace ", lhs_name, " in .GlobalEnv")) 
   if(rpl){
     assign(lhs_name, rhs, envir = .GlobalEnv)
   }
  }else{
    assign(lhs_name, rhs, envir = .GlobalEnv)
  }
}
# > x %a% 1
# > x %a% 2
# Do you want to replace x in .GlobalEnv (Yes/no/cancel) Yes
# > x
# [1] 2
like image 160
DaveArmstrong Avatar answered Sep 02 '25 16:09

DaveArmstrong