Boxes, closures and cloning

Hello, I am learning Rust and I am trying to write a super simple interpreter as a practice project. I have hit a problem which starts with the following code:

use std::collections::HashMap;

struct Evaluator {
    commands: HashMap<String, CommandPtr>,
}

type CommandFn = Fn(&mut Evaluator, &[&str]) -> bool;
type CommandPtr = Box<CommandFn>;

fn hello(_: &mut Evaluator, _: &[&str]) -> bool {
    println!("Hello world");
    return true;
}

fn main() {
    let mut eval = Evaluator { commands: HashMap::new() };
    eval.commands.insert(String::from("hello"), Box::new(hello));
    // Call something in the evaluator - this bit is troublesome
    if let Some(ref cmd) = eval.commands.get("hello") {
        cmd(&mut eval, &["World"]);
    }

}

Link to playground.

The compiler error is:

error[E0502]: cannot borrow `eval` as mutable because `eval.commands` is also borrowed as immutable

This problem has been driving me crazy as I end up in a loop of problems:

  • To please the borrow monster I have to clone the command.
  • I can't make CommandFn cloneable because it is a closure.
  • I can't use my own cloneable trait for the command because of E0038.

Can anyone help me out of this maze?

Also, I don't understand the background behind the E0038 error. My mental model of traits is that they are like interfaces in Java or virtual methods in C++. However, if my mental model was correct, inheriting a trait would not prevent me from having a pointer to it -- something more must be happening.

Thanks in advance for any help.

No, you don't. As you noted, you can't Clone in this circumstance, although you can write a custom cloning trait (you want something that returns Box<Self>, not Self, but I digress).

The "proper" solution to this is to just stop trying to violate the aliasing rules in the first place. One way you can do this is to split Evaluator into commands and everything_else, then pass everything_else to each command.

Or, if commands need access to commands (which would be dangerous), you can remove the command in question from commands, invoke it, then put it back afterwards.

You could also potentially play games with cells, but you should probably try one of those two, first.

Edit: Continuing after I got called away.

Only at a surface level. Traits are primarily compile-time interface descriptions. As a secondary function, traits can have corresponding "object types" which provide dynamic, run-time dispatch (like interfaces in Java). However, traits can contain things that the compiler absolutely cannot dispatch dynamically. Clone::clone is an example of this; it returns Self, which is going to be a different size depending on the implementing type. If it's a difference size, the compiler simply cannot produce a single, consistent binary interface for that method.

The core team decided a while ago that, if a trait contains even a single thing that can't be included in the corresponding object type vtable, it disqualifies the entire trait. This is to prevent people from writing an API that uses &Clone, only to discover later on that they can't actually call the clone method on one of those.

Also, traits don't have inheritance. trait A: B does not mean A inherits B, it means implementing A requires that you also implement B. This distinction is important because you can also have trait B: A.

2 Likes