Key Ideas
Treating external SEXP and owned SEXP differently
Savvy is opinionated in many points. Among these, one thing I think should be explained first is that savvy uses separate types for SEXP passed from outside and that created within Rust function. The former, external SEXP, is read-only, and the latter, owned SEXP, is writable. Here's the list:
R type | Read-only version | Writable version |
---|---|---|
INTSXP (integer) | IntegerSexp | OwnedIntegerSexp |
REALSXP (double) | RealSexp | OwnedRealSexp |
RAWSXP (raw) | RawSexp | OwnedRawSexp |
LGLSXP (logical) | LogicalSexp | OwnedLogicalSexp |
STRSXP (character) | StringSexp | OwnedStringSexp |
VECSXP (list) | ListSexp | OwnedListSexp |
EXTPTRSXP (external pointer) | ExternalPointerSexp | n/a |
CPLXSXP (complex)1 | ComplexSexp | OwnedComplexSexp |
Complex is optionally supported under feature flag complex
You might wonder why this is needed when we can just use mut
to distinguish
the difference of mutability. I mainly had two motivations for this:
- avoid unnecessary protection: an external SEXP are already protected by the caller, while an owned SEXP needs to be protected by ourselves.
- avoid unnecessary ALTREP checks: an external SEXP can be ALTREP, so it's better to handle them in ALTREP-aware way, while an owned SEXP is not.
This would be a bit lengthy, so let's skip here. You can read the details on my blog post. But, one correction is that I found the second reason might not be very important because a benchmark showed it's more efficient to be non-ALTREP-aware in most of the cases. Actually, the current implementation of savvy is non-ALTREP-aware for int, real, and logical (See #18).
No implicit conversions
Savvy doesn't provide conversion between types unless you do explicitly. For
example, you cannot supply a double vector to a function with a IntegerSexp
argument.
#[savvy]
fn identity_int(x: IntegerSexp) -> savvy::Result<savvy::Sexp> {
let mut out = OwnedIntegerSexp::new(x.len())?;
for (i, &v) in x.iter().enumerate() {
out[i] = v;
}
out.into()
}
identity_int(c(1, 2))
#> Error in identity_int(c(1, 2)) :
#> Unexpected type: Cannot convert double to integer
While you probably feel this is inconvenient, this is also a design decision. My concerns on supporting these conversion are
- Complexity. It would make savvy's spec and implemenatation complicated.
- Hidden allocation. Conversion requires a new allocation for storing the converted values, which might be unhappy in some cases.
So, you have to write some wrapper R function like below. This might feel a bit tiring, but, in general, please do not avoid writing R code. Since you are creating an R package, there's a lot you can do in R code instead of making things complicated in Rust code. Especially, it's easier on R's side to show user-friendly error messages.
identity_int_wrapper <- function(x) {
x <- vctrs::vec_cast(x, integer())
identity_int(x)
}
Alternatively, you can use NumericSexp
as input. This provides a method to
convert the input either to i32
or to f64
on the fly. For more details,
please read the section about NumericSexp
#[savvy]
fn identity_num(x: NumericSexp) -> savvy::Result<savvy::Sexp> {
let mut out = OwnedIntegerSexp::new(x.len())?;
for (i, &v) in x.iter_i32().enumerate() {
out[i] = v;
}
out.into()
}