Cargo.toml
#[dependenices]
extender-api = { version = "*", features = ["serde"] }
serde
integrationOne of the most widely used rust crates is the serialization and deserialization crate serde. It enables rust developers to write their custom structs to many different file formats such as json, toml, yaml, csv, and many more as well as read directly from them.
extendr
provides a serde
feature that can convert R objects into structs and struct into R objects.
First, modify your Cargo.toml
to include the serde feature.
For this example we will have a Point
struct with two fields, x
, and y
. In your lib.rs
include:
This defines a Point
struct. However, you may want to be able to use an R object to represent that point. To deserialize R objects into Rust, use extendr_api::deserializer::from_robj
. For a basic example we can deserialize an Robj
into the Point
.
To represent a struct, a named list has to be used. Each name must correspond with the field name of the struct. In this case these are x
and y
.
To serialize R objects you must use extendr_api::serializer::to_robj
this will take a serde-compatible struct and convert it into a corresponding R object.
This function will parse a list into a point and then return the Point
as an R object as well doing a round trip deserialization and serialization process.
You may find your self wanting to deserialize many structs at once from vectors. For example, if you have a data.frame
with 2 columns x
and y
you may want to deserialize this into a Vec<Point>
. To your dismay you will find this not actually possible.
For example we can create a function replicate_point()
.
This will create a Vec<Point>
with the size of n
. If you serialize this to R you will get a list of lists where each sub-list is a named-list with elements x
and y
. This is expected. And is quite like how you would expect something to be serialized into json or yaml for example.
When providing a data.frame
, a closer analogue would be a struct with vectors for their fields like a MultiPoint
struct
and for the sake of demonstration we can create a make_multipoint()
function:
This function can be used to parse a data.frame
into a MultiPoint
.
TryFrom
One of the benefits and challenges of rust is that it requires us to be explicit. Adding another language into play makes it all the more confusing! In many cases there isn’t a 1:1 mapping from Rust to R as you have seen the Point
and MultiPoint
. One way to simplify this would be to use a TryFrom
trait implementation. This is discussed in more detail in another part of the user guide.
Rather than use serde to do the conversion for you, you probably want a custom TryFrom
trait implementation. Here we define an MPoint
tuple struct and then implement TryFrom<Robj>
for it.
pub struct MPoint(Vec<Point>);
impl TryFrom<Robj> for MPoint {
type Error = Error;
fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
let point_df = List::try_from(&value)?;
let x_vec = Doubles::try_from(point_df.dollar("x")?)?;
let y_vec = Doubles::try_from(point_df.dollar("y")?)?;
let inner = x_vec.into_iter().zip(y_vec.into_iter()).map(|(x, y)| {
Point {
x: x.inner(),
y: y.inner()
}
}).collect::<Vec<_>>();
Ok(MPoint(inner))
}
}
This gives us the benefit of being able to pass the struct type directly into the function. Here we create a function centroid()
to calculate the centroid of the MPoint
struct directly. We use to_robj()
to convert it back to an Robj
.
This function can be used with a data.frame
because we implemented the TryFrom
trait.