Using HashMaps

In addition to vectors and lists, extendr supports using Rust’s HashMap type as function arguments. This allows you to work with R named lists using Rust’s hash map data structure.

Note

A HashMap is Rust’s implementation of a hash table. It stores key-value pairs and provides fast lookup, insertion, and deletion operations. Unlike R’s named lists or vectors, HashMaps do not maintain any particular ordering of their elements.

Basic HashMap Type Mapping

The table below shows common HashMap types that can be used with extendr:

R type Rust type Description
list(a = 1L, b = 2L) HashMap<String, i32> Named list with integer values
list(a = 1.0, b = 2.0) HashMap<String, f64> Named list with double values
list(a = "x", b = "y") HashMap<String, String> Named list with character values
list(a = TRUE, b = FALSE) HashMap<String, bool> Named list with logical values
list(a = list(), b = 1) HashMap<String, Robj> Named list with mixed types
ImportantHashMap Ordering and Duplicate Names

There are two important behaviors to be aware of when using HashMaps with R lists:

  1. Unordered: HashMaps do not maintain insertion order. When you convert a HashMap back to an R list using List::from_hashmap(), the order of elements may differ from the original input.

  2. Duplicate Names: If an R list contains duplicate names (e.g., list(x = 1, x = 2)), only the last value will be retained in the HashMap. In this example, the HashMap would contain ("x", 2).

Using HashMaps in Functions

To use a HashMap in your extendr functions, you need to import std::collections::HashMap and specify it as a function argument type. Here’s a simple example that takes a named list of integers:

use std::collections::HashMap;

#[extendr]
fn test_hm_i32(mut x: HashMap<String, i32>) -> List {
    x.insert("inserted_value".to_string(), 314);
    List::from_hashmap(x).unwrap()
}

This function accepts a named list of integers, adds a new key-value pair, and returns the modified list back to R using List::from_hashmap().

test_hm_i32(list(a = 1L, b = 2L, c = 3L))
#> $inserted_value
#> [1] 314
#> 
#> $a
#> [1] 1
#> 
#> $c
#> [1] 3
#> 
#> $b
#> [1] 2

Notice that the order of elements in the returned list may differ from the input order due to HashMap’s unordered nature.

Working with Mixed Types

When working with named lists that contain different types of values, use HashMap<String, Robj> to accept any R object:

use std::collections::HashMap;

#[extendr]
fn test_hm_string(mut x: HashMap<String, Robj>) -> List {
    x.insert("inserted_value".to_string(), List::new(0).into());
    List::from_hashmap(x).unwrap()
}
test_hm_string(
  list(
    a = 1L,
    b = "hello",
    c = c(1.0, 2.0, 3.0)
  )
)
#> $inserted_value
#> list()
#> 
#> $b
#> [1] "hello"
#> 
#> $a
#> [1] 1
#> 
#> $c
#> [1] 1 2 3

Custom Types with TryFrom

Similar to the examples in the serde integration guide, you can use HashMaps with custom types by implementing the TryFrom<Robj> trait. This is particularly useful when you want to work with complex data structures.

Here’s an example using a custom Point struct:

View TryFrom trait implementations
use std::collections::HashMap;

struct Point {
    x: f64,
    y: f64,
}

impl TryFrom<Robj> for Point {
    type Error = Error;

    fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
        let inner_vec = Doubles::try_from(value)?;
        let x = inner_vec[0].inner();
        let y = inner_vec[1].inner();
        Ok(Point { x, y })
    }
}

impl From<Point> for Doubles {
    fn from(value: Point) -> Self {
        Doubles::from_values([value.x, value.y])
    }
}

impl From<Point> for Robj {
    fn from(value: Point) -> Self {
        Robj::from(Doubles::from(value))
    }
}
#[extendr]
fn test_hm_custom_try_from(mut x: HashMap<&str, Point>) -> List {
    x.insert("inserted_value", Point { x: 3.0, y: 0.1415 });
    List::from_hashmap(x).unwrap()
}

This function accepts a named list where each element is a numeric vector of length 2, which gets converted to a Point struct:

test_hm_custom_try_from(
  list(
    origin = c(0.0, 0.0),
    point_a = c(1.0, 2.0),
    point_b = c(3.0, 4.0)
  )
)
#> $origin
#> [1] 0 0
#> 
#> $point_b
#> [1] 3 4
#> 
#> $inserted_value
#> [1] 3.0000 0.1415
#> 
#> $point_a
#> [1] 1 2

See Also