How to map Rust enums to R factors and character strings.

miniextendr provides two complementary systems for enum-like types:

SystemR RepresentationPartial MatchDefaultUse Case
RFactorFactor (integer + levels)Noβ€”Categorical data for table(), lm(), etc.
MatchArgCharacter scalarYesFirst choiceParameter validation (match.arg() style)

πŸ”—RFactor β€” Enum as R Factor

Maps a Rust enum to an R factor with levels. Each variant becomes a level.

#[derive(Copy, Clone, RFactor)]
pub enum Color {
    Red,    // level index 1
    Green,  // level index 2
    Blue,   // level index 3
}

Use in functions:

#[miniextendr]
pub fn describe(color: Color) -> &'static str {
    match color {
        Color::Red => "warm",
        Color::Green => "cool",
        Color::Blue => "cool",
    }
}

#[miniextendr]
pub fn favorite() -> Color {
    Color::Blue
}

From R:

describe(factor("Red", levels = c("Red", "Green", "Blue")))
# [1] "warm"

favorite()
# [1] Blue
# Levels: Red Green Blue

πŸ”—Rename Variants

#[derive(Copy, Clone, RFactor)]
#[r_factor(rename_all = "snake_case")]
pub enum Status {
    InProgress,   // level: "in_progress"
    Completed,    // level: "completed"
    NotStarted,   // level: "not_started"
}

#[derive(Copy, Clone, RFactor)]
pub enum Priority {
    #[r_factor(rename = "lo")]
    Low,
    #[r_factor(rename = "med")]
    Medium,
    #[r_factor(rename = "hi")]
    High,
}

Supported rename_all values: snake_case, kebab-case, lower, upper.

πŸ”—Factor Vectors

Use FactorVec<T> for vectors and FactorOptionVec<T> for vectors with NA:

use miniextendr_api::{FactorVec, FactorOptionVec};

#[miniextendr]
pub fn all_colors() -> FactorVec<Color> {
    FactorVec(vec![Color::Red, Color::Green, Color::Blue])
}

#[miniextendr]
pub fn parse_colors(input: FactorOptionVec<Color>) -> Vec<&'static str> {
    input.0.iter().map(|c| match c {
        Some(Color::Red) => "red",
        Some(Color::Green) => "green",
        Some(Color::Blue) => "blue",
        None => "NA",
    }).collect()
}

From R:

all_colors()
# [1] Red   Green Blue
# Levels: Red Green Blue

x <- factor(c("Red", NA, "Blue"), levels = c("Red", "Green", "Blue"))
parse_colors(x)
# [1] "red" "NA"  "blue"

πŸ”—Caching

The #[derive(RFactor)] macro generates a OnceLock-cached levels STRSXP. The levels string vector is allocated once and reused for all subsequent conversions, giving ~4x speedup for single-value conversions.

πŸ”—Via #[miniextendr]

Instead of #[derive(RFactor)], you can use the attribute macro:

#[miniextendr]
#[derive(Copy, Clone)]
pub enum Color { Red, Green, Blue }

This is equivalent β€” #[miniextendr] on a fieldless enum dispatches to the same RFactor derive internally.


πŸ”—MatchArg β€” Enum as String Parameter

Maps a Rust enum to R character strings with match.arg() validation. Supports partial matching and defaults to the first variant when NULL is passed.

#[derive(Copy, Clone, MatchArg)]
pub enum Mode {
    Fast,    // choice: "Fast"
    Safe,    // choice: "Safe"
    Debug,   // choice: "Debug"
}

Use in functions:

#[miniextendr]
pub fn run(mode: Mode) -> String {
    match mode {
        Mode::Fast => "running fast".into(),
        Mode::Safe => "running safe".into(),
        Mode::Debug => "running debug".into(),
    }
}

The generated R wrapper includes match.arg() validation:

run <- function(mode = NULL) {
  .__mx_choices_mode <- .Call(C_run__match_arg_choices__mode)
  mode <- if (is.factor(mode)) as.character(mode) else mode
  mode <- base::match.arg(mode, .__mx_choices_mode)
  .Call(C_run, mode)
}

From R:

run("Fast")       # exact match
run("F")          # partial match β†’ "Fast"
run()             # NULL β†’ default (first choice: "Fast")
run("Saf")        # partial match β†’ "Safe"
run("X")          # Error: 'arg' should be one of "Fast", "Safe", "Debug"

πŸ”—Rename Variants

Same syntax as RFactor but with #[match_arg(...)]:

#[derive(Copy, Clone, MatchArg)]
#[match_arg(rename_all = "snake_case")]
pub enum BuildStatus {
    InProgress,    // choice: "in_progress"
    Completed,     // choice: "completed"
}

#[derive(Copy, Clone, MatchArg)]
pub enum Priority {
    #[match_arg(rename = "lo")]  Low,
    #[match_arg(rename = "med")] Medium,
    #[match_arg(rename = "hi")]  High,
}

πŸ”—Via #[miniextendr]

#[miniextendr(match_arg)]
#[derive(Copy, Clone)]
pub enum Mode { Fast, Safe, Debug }

πŸ”—Inline String Choices

For simple cases where you don’t need an enum, use choices(...) on a &str parameter:

#[miniextendr]
pub fn correlate(
    x: f64, y: f64,
    #[miniextendr(choices("pearson", "kendall", "spearman"))] method: &str,
) -> String {
    format!("method={}, cor={}", method, x * y)
}

πŸ”—MatchArg as Base Trait

MatchArg is the base trait for all enum-like types. RFactor requires MatchArg as a supertrait, so any RFactor type also has MatchArg::CHOICES, from_choice(), and to_choice(). Use MatchArg as a bound for generic code over both systems:

use miniextendr_api::MatchArg;

fn describe_choices<T: MatchArg>() -> String {
    T::CHOICES.join(", ")
}

fn lookup<T: MatchArg>(choice: &str) -> Option<T> {
    T::from_choice(choice)
}

πŸ”—Comparison Table

FeatureRFactorMatchArg
R storagefactor(1, levels=c(...))"Fast" (character)
ValidationType check (is factor with correct levels)match.arg() with partial matching
Default on NULLErrorFirst choice
Vec supportFactorVec<T>, FactorOptionVec<T>Single values only
Partial matchingNoYes ("F" β†’ "Fast")
Factor inputNativeConverted to character first
Use caseCategorical dataParameter selection

πŸ”—When to Use Which

RFactor when:

  • Data is categorical (colors, species, status codes)
  • Working with R functions expecting factors (table(), lm(), ggplot2)
  • Need vector support with NA handling
  • Factor level ordering matters

MatchArg when:

  • Building an API with string-based options
  • Want R’s match.arg() partial matching and error messages
  • Want a default value when the argument is omitted
  • Validating user input parameters

πŸ”—See Also