This repository has been archived by the owner on Jun 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from extendr/simple-conversions
a tutorial for the most basic of basics re: conversion
- Loading branch information
Showing
3 changed files
with
297 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"hash": "7063db2f2617e622c0b096e04a331149", | ||
"result": { | ||
"engine": "knitr", | ||
"markdown": "---\ntitle: \"Using Rust types in R\"\n---\n\n::: {.cell}\n\n:::\n\n\nThis tutorial demonstrates some of the basics of passing data types back and\nforth between Rust and R. This includes all of the following:\n\n- Passing scalar types between R and Rust.\n- Passing vector types between R and Rust.\n- Printing from Rust to the console in R.\n- Handling missing values in Rust (a primer).\n\nWe'll start with examples showing how to pass R types as explicit Rust types.\nThis is useful for demonstration purposes, but it does ignore one very very big\nissue, and that's missing values. Rust data types do not allow for missing\nvalues, so they have to be handled carefully. Fortunately, extendr offers its\nown data types built on top of the Rust types to do that for you. For this\nreason, **it is strongly recommended that you work with the extendr types\nwherever possible.** However, when first getting comfortable with extendr, \nand possible even Rust, it may feel more comfortable to work with Rust\nnative types. \n\n## Scalar Type Mapping with Rust Types\n\nIn R, there is no such thing as a scalar value. Everything is a vector.\nWhen using a scalar value in R, that is really a length one vector. In Rust,\nhowever, scalar values are the building blocks of everything. \n\nBelow is a mapping of scalar values between R, extendr, and Rust. \n\n\n| R type | extendr type | Rust type |\n|----------------|--------------|----------------|\n| `integer(1)` | `Rint` | `i32` |\n| `double(1)` | `Rfloat` | `f64` |\n| `logical(1)` | `Rbool` | `bool` |\n| `complex(1)` | `Rcplx` | `Complex<f64>` |\n| `character(1)` | `Rstr` | `String` |\n\nTo see how these scalars get passed back and forth between Rust and R,\nwe'll first explore Rust's `f64` value which is a 64-bit float. This is \nequivalent to R's `double(1)`. We'll write a very simple Rust function that \nprints the value of the input and does not return anything. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn scalar_double(x: f64) { \n rprintln!(\"The value of x is {x}\"); \n}\n```\n:::\n\n\n::: callout-note\nNote the use of `rprintln!()` instead of the `println!()` macro.\nUsing `println!()` will not always be captured by the R console. Using\n`rprintln!()` will ensure that it is. \n:::\n\nIf you are not working inside of an extendr R package, you can create this function locally\nusing `rextendr::rust_function()`.\n\n```r\nrextendr::rust_function(\"\nfn scalar_double(x: f64) { \n rprintln!(\"The value of x is {x}\"); \n}\n\")\n```\n\nTry calling this function on a single double value. \n\n\n::: {.cell}\n\n```{.r .cell-code}\nscalar_double(4.2)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nThe value of x is 4.2\n```\n\n\n:::\n:::\n\n\nA couple of things to note with this example. First, `x: f64` tells Rust that\nthe type of `x` being passed to the function is a single double vector or \"float\"\nvalue. Second, `rprintln!(\"{}\", x);` is an extendr macro (the give-away for this\nis the `!`) that makes it easier to print information from Rust to the console\nin R. R users will perhaps notice that the syntax is vaguely `{glue}`-like in\nthat the value of x is inserted into the curly brackets.\n\nNow, what if, rather than printing the value of `x` to the R console, we wanted\ninstead to return that value to R? To do that, we just need to let Rust know\nwhat type is being returned by our function. This is done with the `-> type`\nnotation. The extendr crate knows how to handle the scalar `f64` type and pass\nit to R as double.\n\n\n::: {.cell}\n\n```{.rust .cell-code}\nfn scalar_double(x: f64) -> f64 { \n x \n}\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nx <- scalar_double(4.2)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nThe value of x is 4.2\n```\n\n\n:::\n\n```{.r .cell-code}\ntypeof(x)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] \"NULL\"\n```\n\n\n:::\n\n```{.r .cell-code}\nx + 1\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nnumeric(0)\n```\n\n\n:::\n:::\n\n\n### Additional examples\n\nWe can extend this example to `i32`, `bool` and `String` values in Rust. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn scalar_integer(x: i32) -> i32 { x }\n\n#[extendr]\nfn scalar_logical(x: bool) -> bool { x }\n\n#[extendr]\nfn scalar_character(x: String) -> String { x }\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nscalar_integer(4L)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 4\n```\n\n\n:::\n\n```{.r .cell-code}\nscalar_logical(TRUE)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] TRUE\n```\n\n\n:::\n\n```{.r .cell-code}\nscalar_character(\"Hello world!\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] \"Hello world!\"\n```\n\n\n:::\n:::\n\n\n## Vector Type Mapping with Rust Types\n\nWhat happens if we try to pass more than one value to `scalar_double()`?\n\n\n::: {.cell}\n\n```{.r .cell-code}\nscalar_double(c(4.2, 1.3, 2.5))\n```\n\n::: {.cell-output .cell-output-error}\n\n```\nError in scalar_double(c(4.2, 1.3, 2.5)): Input must be of length 1. Vector of length >1 given.\n```\n\n\n:::\n:::\n\n\nIt errors because the function expects a scalar of the `f64` type, not a vector\nof `f64`. \n\nIn this section, we show you how to pass Rust vectors between R and Rust.\n\n::: callout-important\nWhile using a Rust vector is possible in some cases, it is strongly\nnot recommended. Instead, extendr types should be used as they provide\naccess directly to R objectes. Whereas using Rust vectors requires \nadditional allocations. \n:::\n\n\nThe syntax is basically the same as with scalars, with just some minor changes.\nWe'll use doubles again to demonstrate this.\n\nFor reference, below are the type of Rust vectors that can be utilized with extendr.\n\n| R type | extendr type | Rust type |\n|---------------|--------------|---------------------|\n| `integer()` | `Integers` | `Vec<i32>` |\n| `double()` | `Doubles` | `Vec<f64>` |\n| `complex()` | `Complexes` | `Vec<Complex<f64>>` |\n| `character()` | `Strings` | `Vec<String>` |\n| `raw()` | `Raw` | `&[u8]` |\n| `logical()` | `Logicals` | |\n| `list()` | `List` | |\n\n::: callout-note\nYou might have anticipated `Vec<bool>` to be a supported Rust \nvector type. This is not possible because in R, logical vectors\ndo not contain _only_ `true` and `false` like Rust's bool type. \nThey also can be an `NA` value which has no corresponding representation\nin Rust. \n:::\n\n\nBelow defines Rust function which takes in a vector of `f64` values and prints them out. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn vector_double(x: Vec<f64>) {\n rprintln!(\"The values of x are {x:?}\");\n}\n```\n:::\n\n\nThat function can be called from R which prints the Debug format of the vector. \n\n::: callout-tip\nRust's vector do not implement the [Display](https://doc.rust-lang.org/std/fmt/trait.Display.html) trait so the debug format (`:?`) is used.\n:::\n\n\n::: {.cell}\n\n```{.r .cell-code}\nvector_double(c(4.2, 1.3, 2.5))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nThe values of x are [4.2, 1.3, 2.5]\n```\n\n\n:::\n:::\n\n\n\nReturning values using Rust follows the same rules as R. You do not need to explicitly return a value as long as the last item in an expression is not followed by a `;`. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn vector_double(x: Vec<f64>) -> Vec<f64> { \n x \n}\n```\n:::\n\n\nCalling the function returns the input as a double vector\n\n::: {.cell}\n\n```{.r .cell-code}\nx <- vector_double(c(4.2, 1.3, 2.5))\ntypeof(x)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] \"double\"\n```\n\n\n:::\n\n```{.r .cell-code}\nx + 1\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 5.2 2.3 3.5\n```\n\n\n:::\n:::\n\n\n### Additional examples\n\nThese same principles can be extended to other supported vector types such as `Vec<i32>` and `Vec<String>`.\n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn vector_integer(x: Vec<i32>) -> Vec<i32> { \n x\n}\n\n#[extendr]\nfn vector_character(x: Vec<String>) -> Vec<String> {\n x \n}\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nvector_integer(c(4L, 6L, 8L))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 4 6 8\n```\n\n\n:::\n\n```{.r .cell-code}\nvector_character(c(\"Hello world!\", \"Hello extendr!\", \"Hello R!\"))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] \"Hello world!\" \"Hello extendr!\" \"Hello R!\" \n```\n\n\n:::\n:::\n\n\n## Missing values\n\nIn Rust, missing values do not exist this in part why using Rust types alone is insufficient. Below a simple function which adds 1 to the input is defined. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr]\nfn plus_one(x: f64) -> f64 { \n x + 1.0 \n}\n```\n:::\n\n\nRunning this using a missing value results in an error. \n\n\n::: {.cell}\n\n```{.r .cell-code}\nplus_one(NA_real_)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\nError in plus_one(NA_real_): Input must not be NA.\n```\n\n\n:::\n:::\n\n\nThese extendr types, however, can be utilized much like a normal `f64` that is `NA` aware. You will see that we have replaced the Rust type\n`f64` with the extendr type `Rfloat`. Since `Rfloat` maps to a scalar value and not vector, the conversion needs to be handled more delicately. The macro was invoked with the `use_try_from = true` argument. This will eventually become the default behavior of extendr. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[extendr(use_try_from = true)]\nfn plus_one(x: Rfloat) -> Rfloat { \n x + 1.0 \n}\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nplus_one(NA_real_)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] NA\n```\n\n\n:::\n\n```{.r .cell-code}\nplus_one(4.2)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 5.2\n```\n\n\n:::\n:::\n\n\nThe combination of these two changes allows us to pass missing values to our `plus_one()` function and return\nmissing values without raising an error.\n", | ||
"supporting": [], | ||
"filters": [ | ||
"rmarkdown/pagebreak.lua" | ||
], | ||
"includes": {}, | ||
"engineDependencies": {}, | ||
"preserve": {}, | ||
"postProcess": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
--- | ||
title: "Using Rust types in R" | ||
freeze: true | ||
--- | ||
|
||
```{r} | ||
#| echo: false | ||
library(rextendr) | ||
``` | ||
|
||
This tutorial demonstrates some of the basics of passing data types back and | ||
forth between Rust and R. This includes all of the following: | ||
|
||
- Passing scalar types between R and Rust. | ||
- Passing vector types between R and Rust. | ||
- Printing from Rust to the console in R. | ||
- Handling missing values in Rust (a primer). | ||
|
||
We'll start with examples showing how to pass R types as explicit Rust types. | ||
This is useful for demonstration purposes, but it does ignore one very very big | ||
issue, and that's missing values. Rust data types do not allow for missing | ||
values, so they have to be handled carefully. Fortunately, extendr offers its | ||
own data types built on top of the Rust types to do that for you. For this | ||
reason, **it is strongly recommended that you work with the extendr types | ||
wherever possible.** However, when first getting comfortable with extendr, | ||
and possible even Rust, it may feel more comfortable to work with Rust | ||
native types. | ||
|
||
## Scalar Type Mapping with Rust Types | ||
|
||
In R, there is no such thing as a scalar value. Everything is a vector. | ||
When using a scalar value in R, that is really a length one vector. In Rust, | ||
however, scalar values are the building blocks of everything. | ||
|
||
Below is a mapping of scalar values between R, extendr, and Rust. | ||
|
||
|
||
| R type | extendr type | Rust type | | ||
|----------------|--------------|----------------| | ||
| `integer(1)` | `Rint` | `i32` | | ||
| `double(1)` | `Rfloat` | `f64` | | ||
| `logical(1)` | `Rbool` | `bool` | | ||
| `complex(1)` | `Rcplx` | `Complex<f64>` | | ||
| `character(1)` | `Rstr` | `String` | | ||
|
||
To see how these scalars get passed back and forth between Rust and R, | ||
we'll first explore Rust's `f64` value which is a 64-bit float. This is | ||
equivalent to R's `double(1)`. We'll write a very simple Rust function that | ||
prints the value of the input and does not return anything. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn scalar_double(x: f64) { | ||
rprintln!("The value of x is {x}"); | ||
} | ||
``` | ||
|
||
::: callout-note | ||
Note the use of `rprintln!()` instead of the `println!()` macro. | ||
Using `println!()` will not always be captured by the R console. Using | ||
`rprintln!()` will ensure that it is. | ||
::: | ||
|
||
If you are not working inside of an extendr R package, you can create this function locally | ||
using `rextendr::rust_function()`. | ||
|
||
```r | ||
rextendr::rust_function(" | ||
fn scalar_double(x: f64) { | ||
rprintln!("The value of x is {x}"); | ||
} | ||
") | ||
``` | ||
|
||
Try calling this function on a single double value. | ||
|
||
```{r} | ||
scalar_double(4.2) | ||
``` | ||
|
||
A couple of things to note with this example. First, `x: f64` tells Rust that | ||
the type of `x` being passed to the function is a single double vector or "float" | ||
value. Second, `rprintln!("{}", x);` is an extendr macro (the give-away for this | ||
is the `!`) that makes it easier to print information from Rust to the console | ||
in R. R users will perhaps notice that the syntax is vaguely `{glue}`-like in | ||
that the value of x is inserted into the curly brackets. | ||
|
||
Now, what if, rather than printing the value of `x` to the R console, we wanted | ||
instead to return that value to R? To do that, we just need to let Rust know | ||
what type is being returned by our function. This is done with the `-> type` | ||
notation. The extendr crate knows how to handle the scalar `f64` type and pass | ||
it to R as double. | ||
|
||
```{extendrsrc} | ||
fn scalar_double(x: f64) -> f64 { | ||
x | ||
} | ||
``` | ||
|
||
```{r} | ||
x <- scalar_double(4.2) | ||
typeof(x) | ||
x + 1 | ||
``` | ||
|
||
### Additional examples | ||
|
||
We can extend this example to `i32`, `bool` and `String` values in Rust. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn scalar_integer(x: i32) -> i32 { x } | ||
#[extendr] | ||
fn scalar_logical(x: bool) -> bool { x } | ||
#[extendr] | ||
fn scalar_character(x: String) -> String { x } | ||
``` | ||
|
||
```{r} | ||
scalar_integer(4L) | ||
scalar_logical(TRUE) | ||
scalar_character("Hello world!") | ||
``` | ||
|
||
## Vector Type Mapping with Rust Types | ||
|
||
What happens if we try to pass more than one value to `scalar_double()`? | ||
|
||
```{r} | ||
#| error: true | ||
scalar_double(c(4.2, 1.3, 2.5)) | ||
``` | ||
|
||
It errors because the function expects a scalar of the `f64` type, not a vector | ||
of `f64`. | ||
|
||
In this section, we show you how to pass Rust vectors between R and Rust. | ||
|
||
::: callout-important | ||
While using a Rust vector is possible in some cases, it is strongly | ||
not recommended. Instead, extendr types should be used as they provide | ||
access directly to R objectes. Whereas using Rust vectors requires | ||
additional allocations. | ||
::: | ||
|
||
|
||
The syntax is basically the same as with scalars, with just some minor changes. | ||
We'll use doubles again to demonstrate this. | ||
|
||
For reference, below are the type of Rust vectors that can be utilized with extendr. | ||
|
||
| R type | extendr type | Rust type | | ||
|---------------|--------------|---------------------| | ||
| `integer()` | `Integers` | `Vec<i32>` | | ||
| `double()` | `Doubles` | `Vec<f64>` | | ||
| `complex()` | `Complexes` | `Vec<Complex<f64>>` | | ||
| `character()` | `Strings` | `Vec<String>` | | ||
| `raw()` | `Raw` | `&[u8]` | | ||
| `logical()` | `Logicals` | | | ||
| `list()` | `List` | | | ||
|
||
::: callout-note | ||
You might have anticipated `Vec<bool>` to be a supported Rust | ||
vector type. This is not possible because in R, logical vectors | ||
do not contain _only_ `true` and `false` like Rust's bool type. | ||
They also can be an `NA` value which has no corresponding representation | ||
in Rust. | ||
::: | ||
|
||
|
||
Below defines Rust function which takes in a vector of `f64` values and prints them out. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn vector_double(x: Vec<f64>) { | ||
rprintln!("The values of x are {x:?}"); | ||
} | ||
``` | ||
|
||
That function can be called from R which prints the Debug format of the vector. | ||
|
||
::: callout-tip | ||
Rust's vector do not implement the [Display](https://doc.rust-lang.org/std/fmt/trait.Display.html) trait so the debug format (`:?`) is used. | ||
::: | ||
|
||
```{r} | ||
vector_double(c(4.2, 1.3, 2.5)) | ||
``` | ||
|
||
|
||
Returning values using Rust follows the same rules as R. You do not need to explicitly return a value as long as the last item in an expression is not followed by a `;`. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn vector_double(x: Vec<f64>) -> Vec<f64> { | ||
x | ||
} | ||
``` | ||
|
||
Calling the function returns the input as a double vector | ||
```{r} | ||
x <- vector_double(c(4.2, 1.3, 2.5)) | ||
typeof(x) | ||
x + 1 | ||
``` | ||
|
||
### Additional examples | ||
|
||
These same principles can be extended to other supported vector types such as `Vec<i32>` and `Vec<String>`. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn vector_integer(x: Vec<i32>) -> Vec<i32> { | ||
x | ||
} | ||
#[extendr] | ||
fn vector_character(x: Vec<String>) -> Vec<String> { | ||
x | ||
} | ||
``` | ||
|
||
```{r} | ||
vector_integer(c(4L, 6L, 8L)) | ||
vector_character(c("Hello world!", "Hello extendr!", "Hello R!")) | ||
``` | ||
|
||
## Missing values | ||
|
||
In Rust, missing values do not exist this in part why using Rust types alone is insufficient. Below a simple function which adds 1 to the input is defined. | ||
|
||
```{extendrsrc} | ||
#[extendr] | ||
fn plus_one(x: f64) -> f64 { | ||
x + 1.0 | ||
} | ||
``` | ||
|
||
Running this using a missing value results in an error. | ||
|
||
```{r} | ||
#| error: true | ||
plus_one(NA_real_) | ||
``` | ||
|
||
These extendr types, however, can be utilized much like a normal `f64` that is `NA` aware. You will see that we have replaced the Rust type | ||
`f64` with the extendr type `Rfloat`. Since `Rfloat` maps to a scalar value and not vector, the conversion needs to be handled more delicately. The macro was invoked with the `use_try_from = true` argument. This will eventually become the default behavior of extendr. | ||
|
||
```{extendrsrc} | ||
#[extendr(use_try_from = true)] | ||
fn plus_one(x: Rfloat) -> Rfloat { | ||
x + 1.0 | ||
} | ||
``` | ||
|
||
```{r} | ||
plus_one(NA_real_) | ||
plus_one(4.2) | ||
``` | ||
|
||
The combination of these two changes allows us to pass missing values to our `plus_one()` function and return | ||
missing values without raising an error. |