Handling Vector Output
Basically, there are two ways to prepare an output to the R session.
1. Create a new R object first and put values on it
An owned SEXP can be allocated by using Owned{type}Sexp::new(). new() takes
the length of the vector as the argument. If you need the same length of vector
as the input, you can pass the len() of the input SEXP.
new() returns Result because the memory allocation can fail in case when the
vector is too large. You can probably just add ? to it to handle the error.
let mut out = OwnedStringSexp::new(x.len())?;
Use set_elt() to put the values one by one. Note that you can also assign
values like out[i] = value for integer and double. See Type-specific
Topics for more details.
for (i, e) in x.iter().enumerate() {
// ...snip...
out.set_elt(i, &format!("{e}_{y}"))?;
}
You can use set_na() to set the specified element as NA. For example, it's a
common case to use this in order to propagate the missingness like below.
for (i, e) in x.iter().enumerate() {
// ...snip...
if e.is_na() {
out.set_na(i)?;
} else {
// ...snip...
}
}
After putting the values to the vector, you can convert it to Result<Sexp> by
into().
/// @export
#[savvy]
fn foo() -> savvy::Result<savvy::Sexp> {
let mut out = OwnedStringSexp::new(x.len())?;
// ...snip...
out.into()
}
2. Convert a Rust vector by methods like try_into()
Another way is to use a Rust vector to store the results and convert it to an R
object at the end of the function. This is also fallible because this anyway
needs to create a new R object under the hood, which can fail. So, this time,
the conversion is try_into(), not into().
// Let's not consider for handling NAs at all for simplicity...
/// @export
#[savvy]
fn times_two(x: IntegerSexp) -> savvy::Result<savvy::Sexp> {
let out: Vec<i32> = x.iter().map(|v| v * 2).collect();
out.try_into()
}
Note that, while this looks handy, this might not be very efficient; for example,
times_two() above allocates a Rust vector, and then copy the values into a new
R vector in try_into(). The copying cost can be innegligible when the vector
is very huge.
try_from_slice()
The same conversions are also available in the form of
Owned{type}Sexp::try_from_slice(). While this says "slice", this accepts
AsRef<[T]>, which means both Vec<T> and &[T] can be used.
For converting the return value, probably try_from() is shorter in most of the
cases. But, sometimes you might find this useful (e.g., the return value is a
list and you need to construct the elements of it).
/// @export
#[savvy]
fn times_two2(x: IntegerSexp) -> savvy::Result<savvy::Sexp> {
let out: Vec<i32> = x.iter().map(|v| v * 2).collect();
let out_sexp: OwnedIntegerSexp::try_from_slice(out);
out_sexp.into()
}
try_from_iter()
If you only have an iterator, try_from_iter() is more efficient. This example
function is the case. The previous examples first collect()ed into a Vec,
but it's not necessary in theory.
/// @export
#[savvy]
fn times_two3(x: IntegerSexp) -> savvy::Result<savvy::Sexp> {
let iter = x.iter().map(|v| v * 2);
let out_sexp: OwnedIntegerSexp::try_from_iter(iter);
out_sexp.into()
}
Note that, if you already have a slice or vec, you should use try_from_slice()
instead of calling iter() on the slice or vec and using try_from_iter(). In
such cases, try_from_slice() is more performant for integer, double, and
complex because it just copies the underlying memory into SEXP rather than
handling the elements one by one.