简体   繁体   中英

Rust safe const iterable associative array

I would like to create a structure that's something like a compile-time immutable map with safely checked keys at compile-time. More generally, I would like an iterable associative array with safe key access.

My first attempt at this was using a const HashMap (such as described here ) but then the keys are not safely accessible:

use phf::{phf_map};

static COUNTRIES: phf::Map<&'static str, &'static str> = phf_map! {
    "US" => "United States",
    "UK" => "United Kingdom",
};

COUNTRIES.get("EU") // no compile-time error

Another option I considered was using an enumerable enum with the strum crate as described here :

use strum::IntoEnumIterator; // 0.17.1
use strum_macros::EnumIter; // 0.17.1

#[derive(Debug, EnumIter)]
enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST,
}

fn main() {
    for direction in Direction::iter() {
        println!("{:?}", direction);
    }
}

This works, except that enum values in rust can only be integers. To assign a different value would require something like implementing a value() function for the enum with a match statement, (such as what's described here ), however this means that any time the developer decides to append a new item, the value function must be updated as well, and rewriting the enum name in two places every time isn't ideal.

My last attempt was to use const s in an impl , like so:

struct MyType {
    value: &'static str
}

impl MyType {
    const ONE: MyType = MyType { value: "one" };
    const TWO: MyType = MyType { value: "two" };
}

This allows single-write implementations and the objects are safely-accessible compile-time constants, however there's no way that I've found to iterate over them (as expressed by work-arounds here ) (although this may be possible with some kind of procedural macro).

I'm coming from a lot of TypeScript where this kind of task is very simple:

const values = {
  one: "one",
  two: "two" // easy property addition
}

values.three; // COMPILE-TIME error
Object.keys(values).forEach(key => {...}) // iteration

Or even in Java where this can be done simply with enums with properties.

I'm aware this smells a bit like an XY problem, but I don't really think it's an absurd thing to ask generally for a safe, iterable, compile-time immutable constant associative array (boy is it a mouthful though). Is this pattern possible in Rust? The fact that I can't find anything on it and that it seems so difficult leads me to believe what I'm doing isn't the best practice for Rust code. In that case, what are the alternatives? If this is a bad design pattern for Rust, what would a good substitute be?

@JakubDóka How would I implement it? I did some looking at procedural macros and couldn't seem to understand how to implement such a macro.

macro_rules! decl_named_iterable_enum {
    (
        // notice how we format input as it should be inputted (good practice)

        // here is the indentifier bound to $name variable, when we later mention it
        // it will be replaced with the passed value
        $name:ident {
            // the `$(...)*` matches 0-infinity of consecutive `...`
            // the `$(...)?` matches 0-1 of `...`
            $($variant:ident $(= $repr:literal)?,)*
        }
    ) => {
        #[derive(Clone, Copy)]
        enum $name {
            // We use the metavar same way we bind it,
            // just ommitting its token type
            $($variant),*
            //         ^ this will insert `,` between the variants
        }

        impl $name {
            // same story just with additional tokens
            pub const VARIANTS: &[Self] = &[$(Self::$variant),*];

            pub const fn name(self) -> &'static str {
                match self {
                    $(
                        // see comments on other macro branches, this si a
                        // common way to handle optional patterns
                        Self::$variant => decl_named_iterable_enum!(@repr $variant $($repr)?),
                    )*
                }
            }
        }
    };

    // this branch will match if literal is present
    // in this case we just ignore the name
    (@repr $name:ident $repr:literal) => {
        $repr
    };

    // fallback for no literal provided,
    // we stringify the name of variant
    (@repr $name:ident) => {
        stringify!($name)
    };
}

// this is how you use the macro, similar to typescript
decl_named_iterable_enum! {
    MyEnum {
        Variant,
        Short = "Long",
    }
}

// some example code collecting names of variants
fn main() {
    let name_list = MyEnum::VARIANTS
        .iter()
        .map(|v| v.name())
        .collect::<Vec<_>>();

    println!("{name_list:?}");
}

// Exercise for you:
//    1. replace `=` for name override with `:`
//    2. add a static `&[&str]` names accessed by `MyEnum::VARIANT_NAMES`

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM