Skip to main content

Module lazy_dots

Module lazy_dots 

Source
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(", ")));
    }
    // ...
}
methodR equivalentforces
len / is_empty...length()nothing
names...names()nothing
is_missing_arg(i)element i of substitute(list(...)) is the empty argumentnothing
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 Errelement i
forward(call)f(<args>, ...), evaluated in the framewhat f forces
try_forward(call)the same, with an R error returned as Errwhat 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() or substitute(list(...)), and the value keeps the result, so a loop over the elements copies the expressions once.

§Restrictions

  • Main thread only: the type is !Send and borrows the .Call() argument for the call.
  • It takes no per-parameter option (default, coerce, match_arg, no_na, …) and no dots = typed_list!(..), which validates a forced list. &LazyDots, Option<LazyDots> and Missing<LazyDots> are compile errors: the parameter is always present, taken by value.
  • A function takes at most one ...: &Dots or LazyDots, 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 and extern "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.
LazyDots
R’s ..., passed unforced: the generated wrapper’s own frame, whose ... binding holds the dots as R matched them.

Functions§

dot_symbol 🔒 ⚠
The symbol ..k for 0-based element i (k = i + 1).
r_index 🔒
An R vector index.
with_dots 🔒
call with ... added last, unless it already passes the dots.