Hi! I’m Simone Vittori, a software developer based in London, UK. I have experience in web development with a variety of programming languages, such as Elixir, Elm, Ruby and Rust. I’m passionate about good software design and I use this blog to write about anything programming related that I find interesting. I hope you enjoy reading it! You can subscribe to the feed, follow me on Twitter or fork me on GitHub.

Inserting values of multiple types in Rust's HashMap

I was building a generic data store with Rust and I needed to implement a heterogeneous collection of keys and values. Essentially what I needed was a dictionary, but with values of dynamic type, like both strings and integers at the same time.

Rust is a statically typed language and, due to the memory safety guarantees we are given, all values of some type must have a known, fixed size at compile time, therefore we are not allowed to create a collection of multiple types. However, dynamically sized types also exist, and in this article I’ll show how to use them.

Say we have a HashMap and we want to add more than one value type to it.

For example:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("a", "1");
    map.insert("b", "2");

    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
}

This prints:

a: 1
b: 2

In the example above, the type of map is HashMap<&str, &str>. In other words, both keys and values are of type &str. What if we want the values to be of type &str and, say, i32?

This won’t work:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("a", "1");
    map.insert("b", 2);

    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
}

If we try it, we get this compile time error:

error[E0308]: mismatched types
  --> src/main.rs

     map.insert("b", 2);
                     ^ expected `&str`, found integer

So how do we insert multiple value types in a HashMap? We have several options, each of them with its own trade-offs.

Option #1: Use an enum

We can define our own enum to model our value type, and insert that into the hashmap:

use std::collections::HashMap;

#[derive(Debug)]
enum Value {
    Str(&'static str),
    Int(i32),
}

fn main() {
    let mut map = HashMap::new();

    map.insert("a", Value::Str("1"));
    map.insert("b", Value::Int(2));

    for (key, value) in &map {
        println!("{}: {:?}", key, value);
    }
}

This prints:

a: Str("1")
b: Int(2)

This is similar to a union type. By inserting values of type Value::*, we are effectively saying that the map can accept types that are either string, integer, or any other composite type we wish to add.

Option #2: Use a Box

We can wrap our types in the Box struct:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("a", Box::new("1"));
    map.insert("b", Box::new(2));

    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
}

This doesn’t compile right away. If we try to run this, we get a “mismatched types” error:

error[E0308]: mismatched types
--> src/main.rs

    map.insert("b", Box::new(2));
                             ^ expected `&str`, found integer

Luckily we can fix this by explicitly declaring the type of our map:

let mut map: HashMap<&str, Box<dyn Display + 'static>> = HashMap::new();

This works because we are actually storing instances of Box, not primitive types; dyn Display means the type of the trait object Display. In this case, Display happens to be a common trait between &str and i32.

use std::collections::HashMap;
use std::fmt::Display;

fn main() {
    let mut map: HashMap<&str, Box<dyn Display + 'static>> = HashMap::new();

    map.insert("a", Box::new("1".to_string()));
    map.insert("b", Box::new(2));

    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
}

You may wonder what would happen if we were to use the type dyn Display without the Box wrapper. If we try that, we’d get this nasty error:

error[E0277]: the size for values of type `(dyn std::fmt::Display + 'static)` cannot be known at compilation time
   --> src/main.rs

     let mut map: HashMap<&str, (dyn Display + 'static)> = HashMap::new();
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

    = help: the trait `std::marker::Sized` is not implemented for `(dyn std::fmt::Display + 'static)`
    = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
    = note: required by `std::collections::HashMap

This error may be confusing at first, but it actually makes sense. The Rust Programming Language book explains this very well in the Advanced Types chapter:

“Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory.”

The Box<T> type is a pointer type. It lets us allocate data on the heap rather than the stack, and keeps a reference to the data in the stack in the form of a pointer, which is of fixed size.

(Not an) Option #3: Use separate maps for each type

Here we’re not actually using a HashMap with separate types, but rather two maps, each with its own type. It’s a bit more verbose and perhaps not the solution you’re looking for, but it’s worth keeping in mind that this works too:

use std::collections::HashMap;

fn main() {
    let mut strings_map = HashMap::new();
    let mut integers_map = HashMap::new();

    strings_map.insert("a", "1");
    integers_map.insert("b", 2);

    for (key, value) in &strings_map {
        println!("{}: {}", key, value);
    }

    for (key, value) in &integers_map {
        println!("{}: {}", key, value);
    }
}

It feels much simpler! And the output is naturally the same:

a: 1
b: 2

Conclusion

Rust is very strict when it comes to polymorphic types. As you’ve seen, there are ways to achieve it, but they don’t feel as straightforward as with other dynamic languages such as Ruby or Python. Sometimes though it’s useful to make one step back and look at the actual problem we’re trying to solve. Once I did that, I realized that I didn’t necessarily have to limit myself to a single data structure, so I went for the last option.

I’m still a beginner with Rust, so I might have missed on a better solution. Trait Objects could be one: I’ve experimented with them, but they weren’t quite was I was looking for. If you have any suggestions or know of other possible solutions, feel free to comment below!


Update: @alilleybrinker on Twitter pointed out two caveats to be aware of. One is about the meaning of the 'static bound: when used on a generic type, any references inside the type must live as long as 'static. However, by adding 'static we are also effectively saying that the values inside the Box won’t contain references. The other caveat is that, when using dyn Display, the original types are erased, so the available methods are only those known from the Display trait.

How to fix “Untracked working tree would be overwritten by merge” error

Let’s say you have two Git repositories: one on GitHub, and one on your computer.
They contain identical (or very similar) files, and what you want to do is “synchronize” them (i.e. make them look exactly the same). Maybe all you need is to download a few missing files from GitHub to your computer, or simply push some changes from your machine to GitHub.

You have tried git pull, but you’re getting this error:

error: Untracked working tree file * would be overwritten by merge.
fatal: read-tree failed

You need the changes, but obviously you don’t want to overwrite or lose any files. Don’t worry, the fix is actually straightforward!

Why am I seeing this error?

The reason is probably because you didn’t clone the repository. In my case, I already had some local files, so instead of running git clone, here’s what I did:

git init
git remote add origin git@github.com:<username>/<reponame>.git

If you try to git pull origin <branch-name>, you might get the “untracked working tree” error.

How do I fix it?

If you have already tried pulling from the remote and it didn’t work, here’s the fix:

git branch --track <branch-name> origin/<branch-name>

For example, if your branch is named master:

git branch --track master origin/master

What this does is simply tell Git that these two branches, master and origin/master, are related to each other, and that it should keep track of the changes between them. Turns out it also fixes the error, since Git can now see that nothing would be overwritten.

Wait — that’s it?

Yes! After running the command above, git status will indeed reveal the differences between the two repositories: your untracked files (i.e. extra files that you only have on your PC) will still be there, and some other files may have been automatically staged for deletion: these are files that are present in the remote repo, but you don’t have locally.

At this point you’ll want to double-check that everything is the way it should be. You may also want to run:

git reset

To get a clean state. Don’t worry, this won’t delete anything at all, it will simply unstage any modification that was applied automatically by Git. You can stage back the changes you care about using git add . — once you are happy, you can finally make a commit and run:

git push

Note there’s no need to specify the origin and the branch name anymore, since the two branches (the local and the remote) are now tracked.


Hopefully this article helped you fix your issue; either way, feel free to ask for help by leaving a comment below.

Happy hacking!

How to enable Dark Mode on macOS with the command line

If you want to toggle between light and dark mode, it can be done with a single shell command:

osascript -e 'tell app "System Events" to tell appearance preferences to set dark mode to not dark mode'

Try it and it will switch the mode immediately. No need to restart or install anything.

How does it work?

It’s AppleScript. dark mode is a boolean value in the user defaults system. not dark mode is the opposite of that value. So, for example, if the value is true, it’s like saying not true (i.e. false), effectively acting like a light switch.

Enjoy the dark!

A pure CSS onclick context menu

Context menus are one of those very useful UI widgets that still haven't reached the HTML spec. There have been attempts, but for now everyone tends to make its own custom implementation.

Especially with the advent of React, the tendency is to write a custom menu component that uses JavaScript to open/close itself, perhaps by using an invisible overlay to detect clicks outside the menu and close it accordingly. This works fine in practice, however it doesn't have to be so complicated. If you need a simple dropdown menu that:

  • Has no dependencies;
  • Can be triggered with a click/tap on any element;
  • Is fully cross-browser;
  • Doesn't need JavaScript!

Then look no further. It's much simpler than you think!

An example

Try clicking this button:

This is done in pure HTML and CSS; the JavaScript is there just to add functionality. Source code below.

The HTML

<button></button>
<nav class="menu">
  <ul>
    <li>
      <button onclick="alert('Hello there!')">
        Display Greeting
      </button>
    </li>
    <li>
      <button onclick="print()">
        Print This Page
      </button>
    </li>
  </ul>
</nav>

The CSS

.menu {
    visibility: hidden;
}

button + .menu:active,
button:focus + .menu {
    visibility: visible;
}

That's the trick: we hide the menu in CSS initially, then show it when the button gets focused and while we're clicking on the menu itself. This is necessary so that the click actually gets registered. That's it! No JS trickery involved.

You can attach event listeners to the menu items, e.g. using onclick or document.addEventListener and they'll work as usual.

Obviously the menu can be opened only by elements that can receive focus, such as buttons. So what about other non-interactive elements? Can we make them focusable too? The answer is yes!

A more complicated example

We want to display a context menu when clicking on the following image:

doge meme
wow, this image is clickable!

The HTML

<figure tabindex="-1">
  <img src="/images/doge.png" />

  <nav class="menu">
    <ul>
      <li>
        <button>Open Image in New Tab</button>
      </li>
      <li>
        <button>Save Image As...</button>
      </li>
      <li>
        <button>Copy Image Address</button>
      </li>
    </ul>
  </nav>
</figure>

The trick here was to add tabindex. This makes the element focusable, so that it can open the menu on click. Note that if the clickable element is a <button> or other interactive content (i.e. any focusable element), then you don't even need this!

I've used a <figure>, but you can use any element you like. Just add tabindex="-1" to make it focusable, if it isn't already. You can place the menu anywhere you want in the HTML, as long as you're able to target it with a CSS selector. Just try not to put a button in a button as that's invalid HTML, although technically it will still work.

The CSS

.menu {
    visibility: hidden;
}

figure:active .menu,
figure:focus .menu {
    visibility: visible;
}

How do I make the menu appear next to the mouse cursor?

You'll need JavaScript, but it's entirely up to you whether you want to do this. Alternatively you could add position: absolute to the menu and just make it appear below (or next to) the element you clicked — no need for JS in this case! Anyway, this did the trick for me:

const img = document.querySelector('.doge');
const menu = document.querySelector('.menu');

img.addEventListener('mousedown', ({ offsetX, offsetY }) => {
    menu.style.top = offsetY + 'px';
    menu.style.left = offsetX + 'px';
});

What about browser support?

It may not work in some very old browsers, so make sure to test it in the browsers you need to support. This MDN page has some info about what happens to the focus of a button when being clicked/tapped on different platforms. I did some tests myself and it seems to work well everywhere, including mobile browsers.

And that's it! I hope you found this useful. If you spot any issues, please do let me know by commenting below!

Blog Archives →