When transitioning to Rust from languages with rich literal syntax (like Python's dictionaries or JavaScript's objects), it is natural to want a concise, expressive syntax for declaring custom key-value collections. You might envision something like:

let my_var: MyStruct = [1 => "Value1", 2 => "Value2"];

However, Rust approaches syntax design strictly. In this article, we'll explore whether custom literal syntax is possible without macros, how Rust macros actually evaluate values at runtime, and the idiomatic patterns used across the Rust ecosystem for custom data structure initialization.

Can You Define Custom Literal Syntax Without Macros?

The short answer is no. In Rust, the core syntax and grammar are fixed by the language parser. You cannot override syntax constructs like brackets ([...]) or define custom operators (like => inside a sequence) for type initialization without using Rust's macro system.

Furthermore, Rust does not have an equivalent to Python's __getitem__ or C++'s user-defined literals for container instantiation. Brackets without a macro invocation always evaluate as an array expression.

Clarifying a Common Misconception: Macros vs. Runtime Values

In the original question, there was a concern: "I want to do this without relying on a macro if at all possible so values can be pulled at runtime instead of compile time."

It is important to understand that Rust declarative macros (macro_rules!) do not require values to be known at compile time. Macros perform code generation (token substitution) during compilation, but the generated code evaluates its inputs at runtime just like regular functions. For instance:

let key = get_runtime_key();
let val = fetch_from_network();

// A declarative macro can easily take variables and runtime expressions
let my_var = my_map![
    key => val,
    1 => "static value"
];

Idiomatic Solutions for Custom Mapping Types

1. The Idiomatic Approach: Implementing FromIterator

Before introducing macros, the most idiomatic Rust pattern to initialize collections from sequences of key-value pairs is implementing FromIterator. This integrates cleanly with standard arrays, tuples, and iterators:

use std::iter::FromIterator;

#[derive(Debug)]
pub struct MyStruct {
    entries: Vec<(i32, String)>,
}

impl MyStruct {
    pub fn new() -> Self {
        MyStruct { entries: Vec::new() }
    }

    pub fn add(&mut self, key: i32, value: impl Into<String>) {
        self.entries.push((key, value.into()));
    }
}

impl<S: Into<String>> FromIterator<(i32, S)> for MyStruct {
    fn from_iter<I: IntoIterator<Item = (i32, S)>>(iter: I) -> Self {
        let mut my_struct = MyStruct::new();
        for (k, v) in iter {
            my_struct.add(k, v);
        }
        my_struct
    }
}

impl<S: Into<String>, const N: usize> From<[(i32, S); N]> for MyStruct {
    fn from(arr: [(i32, S); N]) -> Self {
        arr.into_iter().collect()
    }
}

With this setup, you can initialize your struct cleanly from an array of tuples using MyStruct::from([...]):

let runtime_val = String::from("Dynamic");
let my_var: MyStruct = MyStruct::from([
    (1, "Static"),
    (2, runtime_val.as_str()),
]);

2. The Exact Syntax Solution: Declarative Macro (macro_rules!)

If you desire the exact key => value syntax, a declarative macro is the standard tool. It delivers the concise syntax you want while running arbitrary runtime code for each element:

#[macro_export]
macro_rules! my_struct {
    // Match key => value pairs separated by commas
    ($( $key:expr => $val:expr ),* $(,)?) => {{
        let mut temp = $crate::MyStruct::new();
        $(
            temp.add($key, $val);
        )*
        temp
    }};
}

You can then instantiate your collection cleanly with runtime values:

fn main() {
    let runtime_id = 42;
    let user_name = "Alice".to_string();

    let my_var = my_struct![
        1 => "Value1",
        runtime_id => user_name,
    ];

    println!("{:?}", my_var);
}

Summary

  • Grammar is rigid: You cannot create arbitrary custom literal syntaxes like [k => v] natively without macros.
  • Macros support runtime values: Macros merely rewrite the AST; the expressions inside them are evaluated when the program runs.
  • Best Practice: Implement FromIterator or From<[(K, V); N]> for the cleanest standard-library integration, and layer a macro_rules! macro on top if you require custom delimiters like =>.