Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling Rust from R: Error "Expected a vector type."

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.

like image 396
bretauv Avatar asked Sep 03 '26 15:09

bretauv


2 Answers

You need a Rust function that accepts both integer and numeric types

1: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_.

More detail

That was the tl;dr. Here is more info. There are three factors at play here:

  1. The : operator means R creates an integer rather than a numeric vector.
  2. Rust is a statically typed language. The function defined in your question expects an array slice of f64 values. More generally, Rust requires function parameters to be an explicit type , or a generic with shared traits.
  3. extendr does not support generics, by design.

integer vs numeric: what a difference a : makes

As 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.

Translating INTSXP and REALSXP into the Rust world

We 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.

Generics

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.

Creating a Rust function that takes integer and numeric types

An 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
like image 181
SamR Avatar answered Sep 06 '26 03:09

SamR


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!

like image 37
Ilia Avatar answered Sep 06 '26 03:09

Ilia



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!