I'm trying to learn how to connect Rust and R (in addition to learning Rust itself) using the rextendr R package and the extendr Rust crate. I came across the error Expected a vector type. when I use : in the input of my R function.
Below is an example with a simple function that returns TRUE if all the values in the input vector are positive, and FALSE otherwise. It seems to work fine when I use c() in the input, but not when I use :.
library(rextendr)
# create a Rust function that checks if all values in the input are
# greater than 0
rust_function(
"fn all_positive(input: &[f64]) -> bool {
let mut out = true;
for i in input.iter() {
if *i <= 0.0 {
out = false;
break
}
}
out
}"
)
#> ℹ build directory: '/tmp/RtmpeoMSEH/file7f971642e99e'
#> ✔ Writing '/tmp/RtmpeoMSEH/file7f971642e99e/target/extendr_wrappers.R'.
# call it from R
all_positive(c(1, 2, 3))
#> [1] TRUE
all_positive(c(0, 1, 2))
#> [1] FALSE
all_positive(1:3)
#> Error in all_positive(1:3): Expected a vector type.
Why is that and how can I fix it?
PS: since I'm beginning in Rust, feel free to mention any other error/non-idiomatic things in the Rust code.
integer and numeric types1:3 creates an integer vector and c(1,2,3) creates a numeric vector. The function in your example expects f64, i.e. numeric input, so does not like 1:3 (which would be i32). You can write a function that accepts both by making your function parameter an Robj, e.g.
rextendr::rust_function(
"fn all_positive(input: Robj) -> extendr_api::scalar::Rbool {
let float_vec: Option<Vec<f64>> = input.as_real_vector();
let int_vec: Option<Vec<i32>> = input.as_integer_vector();
match float_vec {
Some(vec) => return extendr_api::scalar::Rbool::from_bool(vec.iter().all(|x: &f64| x > &0.0)),
None => (),
}
match int_vec {
Some(vec) => return extendr_api::scalar::Rbool::from_bool(vec.iter().all(|x: &i32| x > &0)),
None => (),
}
extendr_api::scalar::Rbool::na_value()
}
"
)
Edit: now this returns an Rbool rather than a Rust bool , so that if you call the function with a non-numeric type, e.g. a character vector, it can return NA_logical_.
That was the tl;dr. Here is more info. There are three factors at play here:
: operator means R creates an integer rather than a numeric vector.f64 values. More generally, Rust requires function parameters to be an explicit type , or a generic with shared traits.extendr does not support generics, by design.integer vs numeric: what a difference a : makesAs pointed out in the comment by Dirk Eddelbuettel, by convention the : operator implies an integer vector (even before ALTREP):
class(c(1,2,3)) # numeric
class(1:3) # integer
We can peek at the underlying C representation using the lobstr package:
lobstr::sxp(1:3)
# <INTSXP[3]> (altrep named:65535)
lobstr::sxp(c(1,2,3))
# <REALSXP[3]> (named:2)
As set out in R Internals, an INTSXP is a block of C int values. A REALSXP is a block of C double values.
INTSXP and REALSXP into the Rust worldWe can use the extendr-api, extendr-engine and extendr-macros crates (v.0.4.0) to see directly from Rust how extendr maps R types, using the R! macro:
fn main() {
test! {
let r_vec = R!("c(1,2,3)")?;
let r_altrep = R!("1:3")?;
println!("{}", Robj::is_altrep(&r_altrep)); // true
println!("{:?}", r_altrep); // [1,2,3]
println!("{:?}", r_vec); // [1.0, 2.0, 3.0]
}
}
This quick-and-dirty debug print confirms that the 1:3 object is printed as a collection of integers (i32), whereas c(1,2,3) is printed as floats, i.e. f64 in the Rust world.
Now we know that we are dealing with different types, my temptation was to do something like:
fn all_positive_iter<I>(r_obj: I) -> bool
where
I: IntoIterator,
{
// some code here
}
However - although this will compile in Rust, it will not compile as an exported extendr function.
integer and numeric typesAn extendr Github issue about the lack of generics support suggests creating a Rust function which takes an Robj, rather than a primitive Rust type, and resolving this in Rust. In this case, we can use the Robj::as_integer_vector() and Robj::as_real_vector methods.
The documentation for these seems to be in progress but we can see from the source that they return the Option<T> type, i.e. either Some(<T>) or None. We can use the match construct to try to convert what we receive from R into both an integer and a float vector, and only do the thing we want to do when we get Some() type.
As an aside, one of the great things about Rust is it's quite similar to R in often being able to use iterators rather than loops. I think we can do this with the main part of your code. We can use code very similar to the iter.all() docs:
let a = [1, 2, 3];
assert!(a.iter().all(|&x| x > 0));
You could also type cast the i32 to f64 if you don't want to have two comparisons, though as we can chain the .iter() in the match statements I haven't bothered.
This will then allow us to call the function defined at the start for both 1:3 and c(1,2,3).
all_positive(c(1, 2, 3))
#> [1] TRUE
all_positive(c(0, 1, 2))
#> [1] FALSE
all_positive(1:3)
#> [1] TRUE
There are some features in the unreleased version of {extendr} and {extendr} that could help you. Unfortunately, this made me realize that extendr still lacks a couple of very important things (we are on it already).
# Using dev rextendr from github
rextendr::rust_source(
code = "
use either::Either::{self, Left, Right};
#[extendr(use_try_from = true)]
fn all_positive(input: Either<Integers, Doubles>) -> bool {
match input {
Left(ints) => ints.iter().all(|x| <Option<i32>>::from(x).unwrap_or(-1i32) >= 0i32),
Right(dbls) => dbls.iter().all(|x| <Option<f64>>::from(x).unwrap_or(-1f64) >= 0f64),
}
}",
features = "either", # Enable `extendr-api` feature `either`
use_dev_extendr = TRUE, # Use `extendr-api` from github
dependencies = list(either = "*"), # Reference `either` crate
)
#> Warning: Found unknown `extendr` feature: "either".
#> i Are you using a development version of `extendr`?
#> i build directory: 'C:/Users/.../AppData/Local/Temp/RtmpeOiZKI/file4a7c70034e74'
#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpeOiZKI/file4a7c70034e74/target/extendr_wrappers.R'
1:10 |> all_positive()
#> [1] TRUE
(-1 * 1:10) |> all_positive()
#> [1] FALSE
c(1, 2, 3, 5.5) |> all_positive()
#> [1] TRUE
c(1, 2, 3, -5.5) |> all_positive()
#> [1] FALSE
Created on 2023-03-08 with reprex v2.0.2
The use_try_from function option is already coming to dev {rextendr}, but the lack of comparison implemented for Rint and Rfloat makes lambdas ugly. This will be soon tackled as well.
Bonus thing: Integers and Doubles are thin wrappers around Robj (SEXP), so there is no extra memory copying on Rust side (apart from allocating a couple of structs to store SEXP). And it consumes altrep without allocation as well!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With