S3 Methods Guide
How to implement S3 generics (print, format, etc.) with #[miniextendr(s3)].
How to implement S3 generics (print, format, etc.) with #[miniextendr(s3)].
πQuick Example
use miniextendr_api::{miniextendr, ExternalPtr};
#[derive(ExternalPtr)]
pub struct Person {
name: String,
age: i32,
}
#[miniextendr(s3)]
impl Person {
/// @param name Name of the person.
/// @param age Age of the person.
pub fn new(name: String, age: i32) -> Self {
Person { name, age }
}
/// Implements format.Person - returns a formatted string.
#[miniextendr(generic = "format")]
pub fn fmt(&self) -> String {
format!("{} (age {})", self.name, self.age)
}
/// Implements print.Person - prints and returns self invisibly.
#[miniextendr(generic = "print")]
pub fn show(&mut self) {
println!("Person: {}, age {}", self.name, self.age);
}
/// Custom method: greet.Person
pub fn greet(&self) -> String {
format!("Hello, I'm {}!", self.name)
}
}
// Registration is automatic via #[miniextendr].
Generated R code:
# Constructor
new_person <- function(name, age) {
structure(.Call(C_Person__new, name, age), class = "Person")
}
# format.Person - returns the string directly
format.Person <- function(x, ...) {
.Call(C_Person__fmt, x)
}
# print.Person - calls Rust, then returns invisible(x)
print.Person <- function(x, ...) {
.Call(C_Person__show, x)
invisible(x)
}
# greet generic + method
greet <- function(x, ...) UseMethod("greet")
greet.Person <- function(x, ...) {
.Call(C_Person__greet, x)
}πKey Concepts
πGeneric Override with #[miniextendr(generic = "...")]
By default, the Rust method name becomes the S3 generic name. Use generic = "..." to
override this, mapping to a different R generic:
// Rust method is `fmt`, but R generic is `format`
#[miniextendr(generic = "format")]
pub fn fmt(&self) -> String { ... }
// Rust method is `show`, but R generic is `print`
#[miniextendr(generic = "print")]
pub fn show(&mut self) { ... }
This is essential for base R generics like print and format where you want your Rust
method to have a more descriptive name.
πDots (...)
All S3 method signatures include ... automatically. You donβt need to declare them
in your Rust signature. The generated R wrapper adds ... to the parameter list:
# Generated: always has (x, ...) or (x, other_params, ...)
format.Person <- function(x, ...) { ... }
greet.Person <- function(x, ...) { ... }
If your Rust method takes extra parameters, they appear between x and ...:
pub fn describe(&self, verbose: bool) -> String { ... }
// β describe.Person <- function(x, verbose, ...) { ... }πConstructor
The constructor is always the method returning Self. It generates a function named
new_<classname_lowercase>() that wraps the result with structure(..., class = "ClassName"):
pub fn new(name: String, age: i32) -> Self { ... }
// β new_person <- function(name, age) {
// structure(.Call(C_Person__new, name, age), class = "Person")
// }πStatic Methods
Methods without self become standalone functions prefixed with the lowercase class name:
pub fn species() -> String { "Homo sapiens".into() }
// β person_species <- function() { .Call(C_Person__species) }πImplementing print
R convention: print() displays output and returns invisible(x) so the object
can be used in pipelines without double-printing.
Recommended pattern: use &mut self returning ():
#[miniextendr(generic = "print")]
pub fn show(&mut self) {
println!("Person: {}, age {}", self.name, self.age);
}
This generates the ChainableMutation return strategy:
print.Person <- function(x, ...) {
.Call(C_Person__show, x)
invisible(x)
}
The &mut self + void return triggers invisible(x) after the .Call(). This
matches the R convention where print() returns the object invisibly.
Why &mut self? The invisible(x) pattern is only generated for &mut self
methods returning (). With &self returning (), the generated code would be
a bare .Call(...) returning NULL, which is functional but doesnβt follow R convention.
If your print method doesnβt actually mutate, using &mut self is a pragmatic
choice to get correct R behavior.
Alternative: &self returning ():
#[miniextendr(generic = "print")]
pub fn show(&self) {
println!("Person: {}, age {}", self.name, self.age);
}
Generates:
print.Person <- function(x, ...) {
.Call(C_Person__show, x)
}
This works (the println! output appears), but the function returns NULL instead
of invisible(x). For most interactive use this is fine, but y <- print(x) gives
NULL rather than x.
πImplementing format
R convention: format() returns a character vector representation. Many R functions
use format() internally (e.g., paste(), cat(format(x))).
Recommended pattern: &self returning String:
#[miniextendr(generic = "format")]
pub fn fmt(&self) -> String {
format!("{} (age {})", self.name, self.age)
}
Generates:
format.Person <- function(x, ...) {
.Call(C_Person__fmt, x)
}
The string is returned directly (visible), which is correct for format().
Tip: Implement format even if you also implement print. Other R code may call
format() on your object (e.g., when building error messages or tibble displays).
πImplementing Both print and format
A complete type should implement both. The standard R pattern is for print to call
format internally, but with miniextendr each method dispatches to separate Rust code:
#[derive(ExternalPtr)]
pub struct Temperature {
celsius: f64,
}
#[miniextendr(s3)]
impl Temperature {
pub fn new(celsius: f64) -> Self {
Temperature { celsius }
}
#[miniextendr(generic = "format")]
pub fn fmt(&self) -> String {
format!("{:.1}Β°C", self.celsius)
}
#[miniextendr(generic = "print")]
pub fn show(&mut self) {
println!("{:.1}Β°C", self.celsius);
}
}
Usage in R:
t <- new_temperature(36.6)
print(t) # 36.6Β°C (returns invisible(t))
format(t) # "36.6Β°C" (returns the string)
cat(format(t), "\n") # 36.6Β°CπStandalone S3 methods (class defined elsewhere)
The #[miniextendr(s3)]-on-impl pattern above owns both the constructor and the methods: the Rust type Person is the S3 class. That is the right tool when Rust holds the canonical data. It is the wrong tool when:
- The class is defined in R (an existing
structure(list(), class = "my_thing")or a package you donβt control). - The class is a vctrs type where the βobjectβ is an R vector with a class attribute, not a Rust struct.
- You want to provide a method for a base R class (
print.data.frame,format.POSIXct) from a Rust extension.
For those cases, drop the impl block and write a plain function:
use miniextendr_api::{miniextendr, SEXP};
/// Implements `format.percent` in Rust.
#[miniextendr(s3(generic = "format", class = "percent"))]
pub fn format_percent(x: SEXP, _dots: ...) -> Vec<String> {
let data: &[f64] = unsafe { x.as_slice::<f64>() };
data.iter().map(|v| format!("{:.1}%", v * 100.0)).collect()
}
What the macro does:
- Emits an R wrapper named
format.percent(the<generic>.<class>convention). - Adds
#' @method format percentto the roxygen block.devtools::document()picks that up and writesS3method(format, percent)intoNAMESPACE. You do not call.S3method()yourself. - Registers the underlying C entry point via
distributed_slice, same as every other#[miniextendr]function.
Your Rust function receives exactly the arguments R dispatches with. For most S3 generics that is (x, ...), so the first param is typed (SEXP, or a concrete type with TryFromSexp) and the last is _dots: ... to absorb the R-level .... If the generic takes more positional arguments (vec_cast(x, to, ...)), list them in order.
πDouble dispatch (vctrs)
A few generics dispatch on two arguments. vctrs calls vec_ptype2(x, y, ...) and resolves the method as vec_ptype2.<class_of_x>.<class_of_y>. Encode that as a dotted class string:
#[miniextendr(s3(generic = "vec_ptype2", class = "percent.percent"))]
pub fn vec_ptype2_percent_percent(_x: SEXP, _y: SEXP, _dots: ...) -> SEXP { ... }
#[miniextendr(s3(generic = "vec_cast", class = "percent.double"))]
pub fn vec_cast_percent_double(x: SEXP, _to: SEXP, _dots: ...) -> SEXP { ... }
The wrapper name becomes vec_ptype2.percent.percent, and the roxygen becomes #' @method vec_ptype2 percent.percent. That is what vctrs expects.
πvctrs @importFrom
The macro recognizes the vctrs generic names (vec_ptype_abbr, vec_proxy, vec_restore, vec_ptype2, vec_cast, etc.) and auto-injects #' @importFrom vctrs <generic> into the roxygen block. This forces vctrs to load before your method is registered, which is necessary for R_GetCCallable lookups (vctrs registers its native generics via ccallable).
For non-vctrs generics from other packages (e.g., tibble::tbl_sum), add the import manually:
/// @importFrom tibble tbl_sum
#[miniextendr(s3(generic = "tbl_sum", class = "my_tbl"))]
pub fn tbl_sum_my_tbl(x: SEXP, _dots: ...) -> Vec<String> { ... }πConstraints
s3(...)requiresclass.genericdefaults to the Rust function name if omitted, but supplying it explicitly is the convention.- Cannot be combined with
r_name = "...". The S3 naming (<generic>.<class>) already fixes the R wrapper name. - The constructor for the class is out of scope for the method function. Either write it in R, or write a separate
#[miniextendr]function that returnsstructure(x, class = "my_class")vianew_vctr(...)or equivalent helpers inminiextendr_api::vctrs.
πWhen to pick which
| You have⦠| Use |
|---|---|
| A Rust struct that owns the data, methods hang off it | #[miniextendr(s3)] impl MyType { ... } |
| An R-side class (vctrs, base R, another package), methods are Rust logic | standalone #[miniextendr(s3(generic = ..., class = ...))] functions |
| A vctrs class authored here | #[miniextendr(vctrs)] on impl for the constructor + static methods, standalone s3(...) functions for the dispatch-on-class-attribute generics |
πSummary Table
| Method | Receiver | Return | Generated R | R Convention |
|---|---|---|---|---|
format | &self | String | .Call(...) | Returns formatted string (visible) |
print | &mut self | () | .Call(...); invisible(x) | Returns self invisibly |
print | &self | () | .Call(...) | Returns NULL (works but unconventional) |
| custom | &self | any | .Call(...) | Returns value directly |
| custom | &mut self | () | .Call(...); invisible(x) | Chainable mutation |
πSee Also
- CLASS_SYSTEMS.md: overview of all 6 class systems (env, R6, S3, S4, S7, vctrs)
- DOTS_TYPED_LIST.md: using dots and typed_list validation
- VCTRS.md: vctrs-compatible S3 classes with
formatmethods