Summary
This RFC suggests a new data structure for Rust, to be implemented later: A Bi-Directional HashMap, or BijectionMap<K, V, S>
where both the key and value are hashable and can be looked up. Additionaly, a TrijectionMap
is also possible to implement lookups of three values.
Motivation
Many times, people would wish for a simple reverse-lookup function to convert their enums to different values, perhaps a String
to their enum values and vice versa. In the current implementation, you would require two hashmaps:
use std::collections::HashMap;
pub enum MyEnum {
VariantA,
VariantB,
VariantC,
VariantD,
// and so on...
}
const STRING_TO_MYENUM_MAP: HashMap<&str, MyEnum> = HashMap::from([("a", MyEnum::VariantA), ("b", MyEnum::VariantB), ("c", MyEnum::VariantC), ("d", MyEnum::VariantD)] /* and so on... */);
const MYENUM_TO_STRING_MAP: HashMap<MyEnum, &str> = HashMap::from([(MyEnum::VariantA, "a"), (MyEnum::VariantB, "b"), (MyEnum::VariantC, "c"), (MyEnum::VariantD, "d")] /* and so on...*/);
This is too cumbersome for many people to use. True, they could implement their own method, but could there be a better solution using just one map? They could define it
use std::collections::BijectionMap;
pub enum MyEnum {
VariantA,
VariantB,
VariantC,
VariantD,
}
const STRING_CONV_MYENUM_MAP: BijectionMap<&str, MyEnum> = BijectionMap::from([("a", MyEnum::VariantA), ("b", MyEnum::VariantB), ("c", MyEnum::VariantC), ("d", MyEnum::VariantD)] /* and so on... */);
and use it here:
let from_string = STRING_CONV_MYENUM_MAP.get_value("a");
let to_string = STRING_CONV_MYENUM_MAP.get_key(MyEnum::VariantA);
A TrijectionMap<V, W, X>
can be used much the same way, except for three-way converters.
Guide-Level Explanation
A BijectionMap
is a Rust collection that maps keys and values together. Unlike a HashMap
, you can look up a BijectionMap
both ways, by key or by value.
Say you're developing a program and you need to check usernames and match them up with user emails and vice versa. You can, obviously, use two hashmaps and develop a reverser function. But why do two if you can use just one map?
let USER_BIDI_MAP = BijectionMap::new::<&str, &str<()
for user in users {
USER_BIDI_MAP.insert(user.username, user.email);
}
let random_user_who_logged_in = "iamaverysmartperson";
let their_email USER_BIDI_MAP.get(random_user_who_logged_in);
It can also be used to convert enums to other values:
pub enum IpResponse<Value, Error> {
Success(Value),
Info(Value),
Err(Error)
}
// let statuscode_equals_ipresponse = blah blah blah
// This can help us get a status code, convert it to an enum, use it in functions, and send it to a user downstream for example
A TrijectionMap
works the same way, but it can access three values at one time!
let tmp_map = TrijectionMap::new::<String, i64, MyEnum>()
tmp_map.insert("Hello", 932, MyEnum::VariantA)
// Many, many insertions later:
println!(tmp_map.get_value2_from_value1("Hello"));
Reference-Level Explanation
BijectionMap<K, V, S>
will have both Keys and Values implement Eq
and Hash
,
but the API is quite similar to HashMap
:
let ikea_bijection_map = BijectionMap::new::<&str, u32>();
ikea_bijection_map.insert("SVALLJARD", 8321789228129);
ikea_bijection_map.insert("STENKOL", 182317198732918);
ikea_bijection_map.insert("VALLHEIOR", 92131831298718);
ikea_bijection_map.insert("HAREDROR", 92311273981273);
// this user wants WELTAEK. Now which one is that?
user.get_key("samanheik288").cart.push_back(ikea_bijection_map.get_key("WELTAEK"));
// one of the delivery agents gave us this product number and this username. What does it match to?
user.get_key("heldros8492").cart.push_back(ikea_bijection_map.get_value(923179812371));
// ATTENTION! SAMANKEIV is no longer being sold!
ikea_bijection_map.remove_key("SAMANKEIV");
// We ran out of 9328193103930 and IKEA will stop selling it here. What do we do?
ikea_bijection_map.remove_value(9328193103930);
// ERWAK is a great product... but is the name being used?
ikea_bijection_map.entry_key("ERWAK").or_insert(21389181329871);
Same for TrijectionMap
:
let id_name_status_map = TrijectionMap::new::<i64, String, u32>();
id_name_status_map.insert(212019, "HiRandomPersonHere!", 1);
id_name_status_map.insert(238239, "SanoMane290", 2);
id_name_status_map.insert(100029, "DoOrDoNotThereIsNoTry", 3);
id_name_status_map.insert(190000, "SoUncivilized1234", 4);
// Later...
id_name_status_map.get_value_1_from_value_2("SoUncivilized1234");
id_name_status_map.remove_value_1(100029);
id_name_status_map.entry_value_2("ErwakBandez").or_insert_val1and3(12983, 389)
Drawbacks
A drawback to this usage is that all entries have to be one-to-one, so all usecases must be evaluated.
Rationale and Alternatives
An alternative is to implement two or three hashmaps. The BijectionMap
reduces memory size and typing, and is easier to understand in terms of being explicit.
Prior art
Unresolved questions
None? This is quite similar to a HashMap.
Future possibilities
If variadic generics are ever stabilised, a new MultidirectionalMap
may be created with variadic generics for the types, to be able to convert between a theoretically infinite number of types.