how to have a fixed "anchor" to the main function environment?

Viewed 40

In a package, I have a main function that calls a lot of other, unexported functions, with a lot of condition checks everywhere.

When using tidyverse error handling, for instance using the awesome {cli} package, you can attach a caller environment that will be used to decorate the function. The documentation is available here.

Here is some example code:

main = function() f1()
f1 = function() f2()
f2 = function() cli::cli_abort("foobar", call=rlang::caller_env())
main()
#> Error in `f1()`:
#> ! foobar

Created on 2022-07-03 by the reprex package (v2.0.1)

The caller is not f2() but its parent f1(), but the error is still not informative as the end-user is not aware of the internal f1() function either.

To have main() as the end caller, I can either write call=rlang::caller_env(2), which is not very maintainable, or pass the caller as an argument in each and every function that stack, which would require a heavy refactor.

Is there a way to have a global variable that would act as a reference to main(), and which I could use anywhere in my code?

2 Answers

Only (hacky) options I can think of:

1: Keep going up 1 parent environment at a time, until you've reached R_GlobalEnv, then go back 1 step.

main = function() f1()
f1 = function() f2()

f2 = function() {
  for(depth in 1:10) if(identical(rlang::caller_env(depth), globalenv())) break
  if(depth == 10) depth = 1
  cli::cli_abort("foobar", call=rlang::caller_env(depth-1))
}
main()

2: Create some sort of identifier yourself, which you can search for

main = function() {
  marker = "hello!"
  f1()
}
f1 = function() f2()
f2 = function() {
  for(depth in 1:10) try( if( get('marker', envir = rlang::caller_env(depth)) == "hello!" ) break, silent = T)
  if(depth == 10) depth = 0
  cli::cli_abort("foobar", call=rlang::caller_env(depth))
}
main()

I finally managed to get it done.

I declare a global environment in my package, but it was not enough as attributing the my_caller variable again caused a lot of bugs.

At the beginning of the main function, I save the current environment in the global environment as a variable, which I can then call anywhere in the following children functions.

Here is the code:

my_caller = rlang::env()

f = function(x, y){
  my_caller$env = rlang::current_env()
  g()
}
g = function(){
  h()
}
h = function(){
  rlang::abort("foobar", call=my_caller$env)
}

f(x=1, y=5)
#> Error in `f()`:
#>   ! foobar
#> Run `rlang::last_error()` to see where the error occurred.
rlang::last_error()
#> <error/rlang_error>
#>   Error in `f()`:
#>   ! foobar
#> ---
#>   Backtrace:
#>   1. global f(x = 1, y = 5)
#> Run `rlang::last_trace()` to see the full context.
rlang::last_trace()
#> <error/rlang_error>
#>   Error in `f()`:
#>   ! foobar
#> ---
#>   Backtrace:
#>   ▆
#> 1. └─global f(x = 1, y = 5)
#> 2.   └─global g()
#> 3.     └─global h()
#> 4.       └─rlang::abort("foobar", call = my_caller$env)
Related