Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Preserve _parent Environment in Async Functions for R6 Methods #64

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dereckmezquita
Copy link

@dereckmezquita dereckmezquita commented Feb 9, 2025

close #63

Background

When an async function created with coro::async is used as an R6 method, the function fails with an error:

Error in api$getData() : object '_parent' not found

This occurs because R6 replaces the function’s original closure environment (which contains the _parent binding used by the coroutine for internal state) with an R6-specific environment that only provides self and private. Consequently, the lookup for _parent (via rlang::env("_parent")) fails, leading to the error.

What I Did

I have minimally modified the generator factory function (generator0()) in the coro package. The key changes are:

  1. Capture and Inject the Environment:
    I capture the original environment in _parent as before. Then, I inject this environment into the function’s formals as a default argument (named .__coro_env_parent__). This ensures that even if R6 replaces the closure environment, the necessary environment object is preserved.

  2. Reference the Injected Environment:
    In the function body, where the original code previously did:

    `_private` <- rlang::env(`_parent`)

    it now uses:

    `_private` <- rlang::env(.__coro_env_parent__)

    This change guarantees that the coroutine’s internal state is available regardless of any environment substitution by R6.

Minimal Reproducable Example

Below is a minimal script that demonstrates the issue and the subsequent fix:

#!/usr/bin/env Rscript
options(error = function() {
    rlang::entrace()
    rlang::last_trace()
})

sessionInfo()
print(paste("coro: ", packageVersion("coro")))
print(paste("promises: ", packageVersion("promises")))
print(paste("later: ", packageVersion("later")))
print(paste("rlang: ", packageVersion("rlang")))
print(paste("R6: ", packageVersion("R6")))

api_data <- function() {
    return(promises::promise(function(resolve, reject) {
        later::later(function() {
            resolve("Hello, API!")
        }, delay = 3)
    }))
}

MyAPI <- R6::R6Class("MyAPI",
    public = list(
        getData = coro::async(function() {
            message("Simulating API call...")
            result <- await(api_data())
            return(result)
        })
    )
)

# Create an instance and call the asynchronous method.
api <- MyAPI$new()

api$getData()$
    then(function(data) {
        message("Data received: ", data)
    })$
    catch(function(e) {
        message("Error: ", conditionMessage(e))
    })

# Run the later event loop until all asynchronous tasks are complete.
while (!later::loop_empty()) {
    later::run_now()
}

With updates I get the expected outputs:

(base) work@Derecks-MacBook-Air kucoin % Rscript research/r6-coro-async.R
- The project is out-of-sync -- use `renv::status()` for details.
R version 4.4.1 (2024-06-14)
Platform: aarch64-apple-darwin23.4.0
Running under: macOS 15.3

Matrix products: default
BLAS:   /opt/homebrew/Cellar/openblas/0.3.27/lib/libopenblasp-r0.3.27.dylib 
LAPACK: /opt/homebrew/Cellar/r/4.4.1/lib/R/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Chicago
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

loaded via a namespace (and not attached):
[1] compiler_4.4.1 tools_4.4.1    renv_1.0.7    
[1] "coro:  1.1.0.9000"
[1] "promises:  1.3.0"
[1] "later:  1.3.2"
[1] "rlang:  1.1.4"
[1] "R6:  2.5.1"
Simulating API call...
Data received: Hello, API!
(base) work@Derecks-MacBook-Air kucoin % 

This change is backwards compatible and should resolve the compatibility issue between coro::async and R6’s environment re-binding.

Please review and let me know if any further adjustments are needed.

Previously, async functions created via coro::async relied on a lexical
binding of _parent to store internal state. When these functions are
used as R6 methods, R6 replaces their closure environment, which loses
the original _parent binding and causes errors such as:
    Error in api$getData() : object '_parent' not found

This commit injects the captured _parent environment as a default formal
parameter (.__coro_env_parent__) in the generator factory (in generator0()).
The async function now retrieves the environment via:
    rlang::env(.__coro_env_parent__)
rather than relying on a missing lexical binding. This change is minimal,
maintains the original formatting, and ensures that async functions work
correctly even when their environments are replaced (e.g., as R6 methods).

Fixes the R6 compatibility issue with coro async functions.
@dereckmezquita
Copy link
Author

I just need some help now on updating the tests and running them:

r$> devtools::test()

ℹ Testing coro| F W  S  OK | Context| 1       38 | async                                                                                                                                                                             Note: no visible binding for global variable 'type' at generator.R:149 
Note: no visible binding for global variable 'state_machine' at generator.R:152 
Note: no visible binding for global variable 'state_machine' at generator.R:153 
Note: no visible binding for global variable 'fmls' at generator.R:171 
Note: no visible binding for global variable 'debugged' at generator.R:205 
Note: no visible binding for global variable 'state_machine' at generator.R:269 
Note: no visible binding for global variable 'type' at generator.R:279| 2       44 | async [1.4s]                                                                                                                                                                      
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Error (test-async.R:235:3): async functions and async generator factories print nicely
Error in `eval(substitute(expr), data, enclos = parent.frame())`: object 'type' not found
Backtrace:1. └─testthat::expect_snapshot(print(fn, internals = TRUE, reproducible = TRUE)) at test-async.R:235:3
 2.   └─rlang::cnd_signal(state$error)

Failure (test-async.R:327:3): async functions do not cause CMD check notes (#40)
`invisible(compiler::cmpfun(async(function() NULL), options = list(suppressAll = FALSE)))` produced output.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
✖ | 2       86 | generator                                                                                                                                                                         
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Error (test-generator.R:28:3): generator factories print nicely
Error in `eval(substitute(expr), data, enclos = parent.frame())`: object 'type' not found
Backtrace:1. └─testthat::expect_snapshot(...) at test-generator.R:28:3
 2.   └─rlang::cnd_signal(state$error)

Failure (test-generator.R:321:3): generators do not cause CMD check notes (#40)
`invisible(compiler::cmpfun(generator(function() NULL), options = list(suppressAll = FALSE)))` produced output.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
✔ |          3 | iterator-adapt|         10 | iterator-for|          3 | iterator [1.0s]                                                                                                                                                                   
✔ |         15 | parser-block|         13 | parser-if|         29 | parser-loop|         54 | parser|         19 | step-reduce|          7 | step                                                                                                                                                                              

══ Results ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Duration: 5.8 s

── Failed tests ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Error (test-async.R:235:3): async functions and async generator factories print nicely
Error in `eval(substitute(expr), data, enclos = parent.frame())`: object 'type' not found
Backtrace:1. └─testthat::expect_snapshot(print(fn, internals = TRUE, reproducible = TRUE)) at test-async.R:235:3
 2.   └─rlang::cnd_signal(state$error)

Failure (test-async.R:327:3): async functions do not cause CMD check notes (#40)
`invisible(compiler::cmpfun(async(function() NULL), options = list(suppressAll = FALSE)))` produced output.

Error (test-generator.R:28:3): generator factories print nicely
Error in `eval(substitute(expr), data, enclos = parent.frame())`: object 'type' not found
Backtrace:1. └─testthat::expect_snapshot(...) at test-generator.R:28:3
 2.   └─rlang::cnd_signal(state$error)

Failure (test-generator.R:321:3): generators do not cause CMD check notes (#40)
`invisible(compiler::cmpfun(generator(function() NULL), options = list(suppressAll = FALSE)))` produced output.

[ FAIL 4 | WARN 0 | SKIP 0 | PASS 283 ]

r$>

r$>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error when using coro::async as an R6 method: object '_parent' not found
1 participant