Oxidization: Nested hashes

This might be an idiosyncrasy of mine, but I tend to use hashes a lot in Ruby. For instance, recently I was working on a code challenge involving ordering food. There were different categories of courses (breakfast, lunch, and dinner) and different categories of foods (main dish, side dish, etc.), and you need both in order to resolve the final dish name (e.g. lunch + side dish = "banchan").

In Ruby, I would have just used nested hashes. Something like this:

menu = {
  breakfast: { ... },
  lunch: { ... },
  dinner: {
    main_dish: "bulgogi",
    side_dish: "banchan",
    drink: "boricha"
  }
}

…and I would use it like this:

# What's for dinner? 🤔
menu[:dinner][:main]
#=> "bulgogi"

I struggled with implementing the same pattern in Rust using HashMaps and Enums. First, the enums:

#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Course {
    Breakfast,
    Lunch,
    Dinner,
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Dish {
    Main,
    Side,
    Drink,
}

Enums have a few advantages over using Ruby symbols as keys in the hashmap. You get IDE and compiler help using enums, whereas in Ruby if you misspell a symbol like, for instance, :dinnner, the code will continue to run, it’ll just explode somewhere down the line as a runtime error. Rust, on the other hand, will complain during compilation if you spell your variant as Course::Dinnner.

Next to create the nested hashmap:

use std::collections::HashMap;
let menu = HashMap::from([
    (
        Course::Breakfast,
        HashMap::from([
            (Dish::Main, "米粥"),
            (Dish::Side, "肉松"),
            (Dish::Drink, "菊花茶"),
        ]),
    ),
    // ...
]);
let item = menu
    .get(&Course::Breakfast)
    .unwrap()
    .get(&Dish::Main)
    .unwrap();

However, this led to a disagreement with the compiler because item in this case is a reference to a reference. Its type is &&str, which was incompatible with the rest of my function signatures (&str), and I couldn’t dereference it for some reason (unfortunately lost to an uncommitted rendition of code). I also wanted menu to be a global static constant that I can use elsewhere, which involved using once_cell, and it was just getting overly complicated.

All this led to a rethinking of the problem. I had two keys and wanted to resolve a label for each combination thereof. The HashMap was an implementation detail. Thankfully, I realized Rust had what I need to do resolution of an arbitrary number and type of keys – pattern matching!

Instead of complicated nested hashmaps, I could have a function get_item() that is just a function that matches on Course and Dish and returns a &str. Since it’s a function, it’s easy to import in other modules. Since it uses pattern matching, it enforces matches, so adding different Courses and Dishes (which I did) results in compiler warnings which helped me shore up the code. Using a built in meant no more importing HashMap and no more chains of get() and unwrap().

fn get_item(course: &Course, dish: &Dish) -> &'static str {
    match (course, dish) {
        (Course::Breakfast, Dish::Main) => "Oatmeal",
        (Course::Lunch, Dish::Main) => "Fried Rice",
        (Course::Dinner, Dish::Main) => "Curry",
        //  ...
    }
}
let item: &str = get_item(&Course::Breakfast, &Dish::Main);

It worked out quite nicely and made me rethink my use of nested hashes, particularly in Rust but also in Ruby, given the recent addition of native pattern matching:

menu = {
  dinner: {
    main: "fried rice",
    side: "green beans",
    drink: "tea"
  }
}
menu in {
  dinner: {
    main: main,
    side: side,
    drink: drink
  }
}
puts "Today we will be eating #{main} with a side of #{side} and #{drink}."
# run.rb:9: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!
Today we will be eating fried rice with a side of green beans and tea

Neat! One less abuse of nested hashes. Though in Ruby it’s noticeably less ergonomic to use pattern matching than it is in Rust, so I may stick with nested hashes for this particular use case in Ruby.

You can find all the Rust code for this post in this playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=03ddd0d98364d7a886decbf08d15ed96

Leave a Reply

Your email address will not be published. Required fields are marked *