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.