Expand description
The dots left unforced: LazyDots.
A &Dots parameter receives list(...), which the generated wrapper
evaluates before the Rust body runs: every element is forced, and an empty
argument (f(a = ), x[1, , , ]) stops the call with R’s “argument is
missing, with no default”. A parameter typed LazyDots is R’s ... too,
but the wrapper passes its own frame, environment(), in place of
list(...), and nothing is evaluated on the way in. The body counts and
names the dots, sees which are empty, reads what was written, and forces
the elements it wants, each in its own environment.
use miniextendr_api::{LazyDots, SEXP, miniextendr};
/// `subset()` for whole rows only: anything in `...` is refused before it
/// is evaluated, so `subset(x, TRUE, select = ID)` reports `select`, not
/// "object 'ID' not found".
#[miniextendr(s3(generic = "subset", class = "my_tbl"))]
pub fn my_tbl_subset(x: SEXP, subset: SEXP, rest: LazyDots) -> Result<SEXP, String> {
if !rest.is_empty() {
let names: Vec<_> = rest.names().into_iter().map(|n| n.unwrap_or("")).collect();
return Err(format!("unsupported arguments: {}", names.join(", ")));
}
// ...
}| method | R equivalent | forces |
|---|---|---|
len / is_empty | ...length() | nothing |
names | ...names() | nothing |
is_missing_arg(i) | element i of substitute(list(...)) is the empty argument | nothing |
expr(i) | element i of substitute(list(...)) | nothing |
force(i) | ..k (k = i + 1) | element i |
try_force(i) | ..k, with an R error returned as Err | element i |
forward(call) | f(<args>, ...), evaluated in the frame | what f forces |
try_forward(call) | the same, with an R error returned as Err | what f forces |
next_method() | NextMethod() | what the next method forces |
Indices are 0-based, as in List::get_index:
element i is R’s ..k with k = i + 1. An index past the end panics
before anything is read or forced for it, as slice indexing does.
§Forwarded dots
Each element keeps the environment it was written in, also when another
function passed its own dots on (g <- function(...) f(x, ...)):
force(i) evaluates ..k in the wrapper’s frame, which forces the
element’s promise in that environment, and expr(i) gives what the caller
of g wrote.
§Passing the dots on
forward evaluates an RCall in the wrapper’s
frame with ... added last (or where RCall::dots put it), so R hands
the callee each element as the promise it matched: unevaluated, with the
environment it was written in. A data-masking function evaluates them
itself, against its own data, as when R code writes f(x, ...):
use miniextendr_api::{LazyDots, SEXP, expression::RCall, miniextendr};
/// `group_by()` on the plain data frame inside the object.
#[miniextendr(s3(generic = "group_by", class = "my_results"))]
pub fn my_results_group_by(data: SEXP, rest: LazyDots) -> Result<SEXP, String> {
let plain = plain_frame(data); // the data frame inside the object
// `dplyr::group_by(plain, ...)`; dplyr is only suggested.
let group_by = unsafe { RCall::namespaced("dplyr", "group_by") }
.map_err(|e| e.to_string())?;
Ok(rest.forward(group_by.arg(plain)))
}next_method evaluates NextMethod() in the
frame. The frame is the S3 method’s own, so R finds the dispatch it is in
and calls the next method with the method’s arguments, the dots passed on
unevaluated.
§Empty and missing elements
is_missing_arg(i) is true only for a
literally empty slot, the element R stores as the missing argument:
f(a = ), the fourth position of x[1, , , ]. That is what R 4.6’s
R_GetDotType() reports as missing.
An element forwarded from a missing argument is not empty: in
g <- function(a) f(1, a) called as g(), element 1 is the promise of
a, so is_missing_arg(1) is false. Forcing it raises R’s own error,
argument "a" is missing, with no default, as list(...) would.
Forcing an empty element raises R’s error for it,
argument "..k" is missing, with no default (class missingArgError
from R 4.6, simpleError before), whose call is the generated function’s
own call. A body that accepts empty arguments checks is_missing_arg(i)
before forcing.
§Conditions
force evaluates through
eval_with_handlers: the caller’s
withCallingHandlers() / suppressWarnings() see what forcing signals,
and an error, or the exit of a tryCatch() handler, unwinds through the
Rust frames (running their destructors) and carries on in R as the
original condition. try_force evaluates through
try_eval_with_handlers:
warnings and messages still reach the caller’s handlers, and an R error
comes back as Err(REvalError) for the body to handle. The calls
behind len, names, is_missing_arg and expr evaluate through
eval_with_handlers too, never a bare Rf_eval.
§Caching
- Forced values are not cached in Rust. R stores a promise’s value in
the promise, so forcing an element twice evaluates it once and the
second
force(i)returns the stored value. - The count and the expressions are read once per value. The first
call that needs them evaluates
...length()orsubstitute(list(...)), and the value keeps the result, so a loop over the elements copies the expressions once.
§Restrictions
- Main thread only: the type is
!Sendand borrows the.Call()argument for the call. - It takes no per-parameter option (
default,coerce,match_arg,no_na, …) and nodots = typed_list!(..), which validates a forced list.&LazyDots,Option<LazyDots>andMissing<LazyDots>are compile errors: the parameter is always present, taken by value. - A function takes at most one
...:&DotsorLazyDots, not both. - Standalone functions,
s3(...)functions, and the impl-block methods of every class system (env, R6, S3, S4, S7, vctrs) accept it. Trait methods andextern "C-unwind"functions do not.
§GC
The frame is a .Call() argument, which R protects for the call, and the
dots are its ... binding: the promises and the values they cache stay
reachable until the call returns. The list substitute(list(...)) gives
is rooted by the LazyDots value (R_PreserveObject) and released when it
drops.
Structs§
- Exprs 🔒
- The expressions
substitute(list(...))gives in the frame, read once. - Lazy
Dots - R’s
..., passed unforced: the generated wrapper’s own frame, whose...binding holds the dots as R matched them.
Functions§
- dot_
symbol 🔒 ⚠ - The symbol
..kfor 0-based elementi(k = i + 1). - r_index 🔒
- An R vector index.
- with_
dots 🔒 callwith...added last, unless it already passes the dots.