How to create a lookup table in an idiomatic way

I need to build a lookup table, and am trying to do this is in the best possible and idiomatic way.

I have come up with this so far:

use std::collections::BTreeMap;

struct ValuesLookup {
    values_lookup: BTreeMap<(String, String), String>,
}

impl ValuesLookup {
    fn insert(&mut self, first: &str, second: &str, third: &str) {
        self.values_lookup.insert((first.to_string(), second.to_string()), third.to_string());
    }
    fn create(&mut self) -> &ValuesLookup {
        self.insert("a", "b", "c");
        self.insert("d", "e", "f");
        self.insert("g", "h", "i");
        self.insert("j", "k", "l");
        self.insert("m", "n", "o");
        self
    }
    fn lookup(&self, arg1: &str, arg2: &str) -> String {
       match self.values_lookup.get(&(arg1.to_string(), arg2.to_string())) {
           Some(lookup) => return lookup.to_string(),
           None         => return "-".to_string(),
       }
    }
}

fn main() {
    let mut a = ValuesLookup { values_lookup: BTreeMap::new() };
    let t = ValuesLookup::create(&mut a);
    // returns l, value found
    println!("{}", t.lookup( "j", "k"));
    // returns -, value not found
    println!("{}", t.lookup( "z", "k"));
}

This works, and already is great.
What I would like is the initialisation of the lookup table which is two lines here:

  • (let mut a) for creating the struct with BTreeMap in the first line
  • and then the insertion of the rows (let t = ValuesLookup::create(&mut a))

To be a single call/line. Is it possible to move that into the impl? Or can this be done better in a totally different way?

Sure:

use std::collections::BTreeMap;

struct ValuesLookup {
    values_lookup: BTreeMap<(String, String), String>,
}

impl ValuesLookup {
    fn new() -> Self {
        let mut me = ValuesLookup { values_lookup: BTreeMap::new() };
        me.insert("a", "b", "c");
        me.insert("d", "e", "f");
        me.insert("g", "h", "i");
        me.insert("j", "k", "l");
        me.insert("m", "n", "o");
        me
    }
    fn insert(&mut self, first: &str, second: &str, third: &str) {
        self.values_lookup.insert((first.to_string(), second.to_string()), third.to_string());
    }
    fn lookup(&self, arg1: &str, arg2: &str) -> String {
       match self.values_lookup.get(&(arg1.to_string(), arg2.to_string())) {
           Some(lookup) => return lookup.to_string(),
           None         => return "-".to_string(),
       }
    }
}

fn main() {
    let t = ValuesLookup::new();
    // returns l, value found
    println!("{}", t.lookup( "j", "k"));
    // returns -, value not found
    println!("{}", t.lookup( "z", "k"));
}
1 Like

Thank you @alice ! So simple and so elegant!

You can change your create function to something like

fn create() -> ValuesLookup
{
        let mut a = ValuesLookup { values_lookup: BTreeMap::new() };
        a.insert("a", "b", "c");
        a.insert("d", "e", "f");
        a.insert("g", "h", "i");
        a.insert("j", "k", "l");
        a.insert("m", "n", "o");
        a
}
1 Like

And don't forget that blocks are expressions in Rust, so this is fine, too:

foo({
        let mut a = ValuesLookup { values_lookup: BTreeMap::new() };
        a.insert("a", "b", "c");
        a.insert("d", "e", "f");
        a.insert("g", "h", "i");
        a.insert("j", "k", "l");
        a.insert("m", "n", "o");
        a
});

@simonbuchan in such case, how can I use the struct with the btreemap, because foo() doesn't have a returning type set?

This line means that there are two allocations during lookup:

       match self.values_lookup.get(&(arg1.to_string(), arg2.to_string())) {

I think it would be better if there wasn't any .to_string() involved during lookup. Fixing this doesn't seem so easy. I think it would require to create a custom struct that you store in the BTreeMap.

I faced a similar problem regarding tuples in mmtkvdb and was able to avoid some unnecessary allocations with this StorableRef trait (in my case).

Edit: I just noticed this problem already exists in the OP. So I could/should have replied to the OP instead.

Simply a reminder and an example that you can initialize and pass a value as an argument without extra statements, which is often handy when dealing with initialization. foo() here would be taking a ValuesLookup and doing something with it.

@jbe I noticed that too, and switched to &str in my code.
But it doesn't matter for the general question.
For many people familiar with Rust it's obvious to create impl's for a struct and peform work in the impl, but this is not existing in traditional/ancient languages, and maybe not extensively documented so that people from an ancient computer language can pick up easy.

@simonbuchan My idea was how to initialise a struct with accompanying code to minimise codelines and have it look elegant along the way. I think that when using what you showed, the work is performed in it's own scope? The foo function doesn't return anything, so how does the last line 'a' (without ';') work?
I am not criticising, I am trying to understand.

Ah. So, what I was saying is that let a = ...; foo(a) is the same as let a = ...; foo({ a }), which is the same as foo({ let a = ...; a }). This can be handy to isolate the initialization, or to keep the control flow clearer, without needing to extract the initialization out into a new function.

In programming language terminology, this is saying a block is an expression, which means it has a value and can be used in other expressions. The value is that of the final expression without a semicolon, the same as in a function body. The same is true for other control flow syntax: if a { b } else { c } has either the value b or c, loop { break a } has the value a.

My earlier example, therefore, was simply a statement that you can create a ValueLookup, initialize it, and pass it to foo() all in the same expression, and it's especially a good thing to remember when you're trying to figure out how to create a type that you can initialize nicely.

1 Like