Skip to content

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$>

@dereckmezquita
Copy link
Author

Hi @lionel- can I get you to review my PR please? I really need to use the package in production code and I would like to contribute.

Please let me know what work needs to be done and I'll do it.

@lionel-
Copy link
Member

lionel- commented Mar 10, 2025

The problem is that this is quite a tricky part of the package. I'll try to find some time to take a look at this if I can.

@dereckmezquita
Copy link
Author

@lionel- thank you for the work and I know you're busy. I depend on this package. As such can you offer guidance and I could do the work.

How can I facilitate this fix?

Reminder: I need to be able to use coro async functions within R6 classes as methods.

@lionel-
Copy link
Member

lionel- commented Jul 23, 2025

yep really busy especially with UseR! and Posit::conf coming and the GA release of Positron.

I keep this PR is an open tab to make sure I'm periodically reminded of it. I'll try to take a look in the coming weeks, hopefully sooner, thanks for your patience and sorry for the delay!

@lionel-
Copy link
Member

lionel- commented Sep 9, 2025

Unfortunately I don't love the idea of passing the environment via formals because that exposes it to users and could produce strange errors when unexpected arguments are passed. Maybe we could pass it through attributes (which we can access via sys.function()), I just worry we're only reporting the problem to contexts where attributes are zapped.

So I'm wondering about allowing regular functions to return async blocks. This would be equivalent to async functions returning regular blocks, an idea from the Rust language.

MyClass <- R6::R6Class(
  classname = "MyClass",

  public = list(
    value = 1:3,

    # Doesn't work because R6 zaps the coro environment
    async_method = async(function() {
      for (i in await_each(self$value)) {
        print(i)
      }
    }),

    # Workaround: return an async block from a regular function.
    # Similar to async blocks in Rust which create a future/promise.
    async_method = function() {
      async({
        for (i in await_each(self$value)) {
          print(i)
        }
      })
    }
  )
)

Like in Rust, async blocks would be lambda boundaries and return() would return from the block, not the enclosing context (or alternatively returning from such a block could be an error).

By the way I assume you're aware of this workaround? https://github.com/tidyverse/ellmer/blob/649b7c599c995be1adf8374938b6c6347dbe9624/R/chat.R#L310-L327

@lionel-
Copy link
Member

lionel- commented Sep 13, 2025

Closed by #67

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

2 participants