0

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 consts 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?

Nick
  • 158
  • 8
  • What runtime error are you getting in your first example? The docs say it should return an `Option`. – Bale Jan 13 '23 at 19:00
  • you can eliminate the double definition with a macro – Jakub Dóka Jan 13 '23 at 19:00
  • If I understood correctly what you want, you might want to wrap an array in a new type that implements Index where T is some enum restricting keys to valid ones. – Ivan C Jan 13 '23 at 19:02
  • @Bale That's my bad, but the point is I would like it to throw a compile-time error and it doesn't. – Nick Jan 13 '23 at 19:03
  • @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. – Nick Jan 13 '23 at 19:06
  • @IvanC I looked into this, and it seems like it would require double-writing code such as in my `values` function example (at least that's what it seems from [this](https://doc.rust-lang.org/std/ops/trait.Index.html#examples), where a lot of rewriting is required). Is there any smarter way to implement this? – Nick Jan 13 '23 at 19:30
  • That's not quite what I meant. Anyways, what I was recommended turns out to be impossible to implement without `#![feature(generic_const_exprs)]` – Ivan C Jan 13 '23 at 19:51
  • @Nick https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=ec35986531a3d71ee8a08de610d2d285 here's something close to my idea – Ivan C Jan 13 '23 at 19:59
  • The solution by @JakubDóka is the best, but just for fun, here's how I'd implement such hashmap using const evaluation: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=fc7120557b72e021cdf92be5118d37ac. – Chayim Friedman Jan 14 '23 at 16:54

1 Answers1

2

@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`

Jakub Dóka
  • 2,477
  • 1
  • 7
  • 12
  • I'll accept this. What I actually ended up going for was [a bit simpler and allowed non-literals but still based on this](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=770f552c31a0f12967cd22869a2aab95). Thanks for your help! – Nick Jan 16 '23 at 23:49