-
Notifications
You must be signed in to change notification settings - Fork 12
Fix: Preserve _parent Environment in Async Functions for R6 Methods
#64
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
Conversation
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.
|
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$> |
|
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. |
|
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. |
|
@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. |
|
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! |
|
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 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 By the way I assume you're aware of this workaround? https://github.com/tidyverse/ellmer/blob/649b7c599c995be1adf8374938b6c6347dbe9624/R/chat.R#L310-L327 |
|
Closed by #67 |
close #63
Background
When an async function created with
coro::asyncis used as an R6 method, the function fails with an error:This occurs because R6 replaces the function’s original closure environment (which contains the
_parentbinding used by the coroutine for internal state) with an R6-specific environment that only providesselfandprivate. Consequently, the lookup for_parent(viarlang::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:Capture and Inject the Environment:
I capture the original environment in
_parentas 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.Reference the Injected Environment:
In the function body, where the original code previously did:
it now uses:
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:
With updates I get the expected outputs:
This change is backwards compatible and should resolve the compatibility issue between
coro::asyncand R6’s environment re-binding.Please review and let me know if any further adjustments are needed.