[newbie] mut with nested structs

Hello all,

I have a problem that I know how to solve in Python, and I'd love some help in making it play nicely with Rust's borrow checker.

In brief, I'm modeling the derivation of specific words. A derivation has terms that we mutate during the program execution and a history that represents the changes made so far. Loosely, the core data structures are:

struct Term {
  text: String,
  metadata: HashSet<String>,
}

struct Derivation {
  terms: Vec<Term>,
  history: Vec<String>,
}

impl Derivation {
  // Updates just the `history` field of `Derivation`. All other data is kept the same.
  pub fn update_history(&mut self) {
    ..
  }
}

In my head, the code should look something like this:

let i: usize = ...;
let t: mut Term = d.terms.get_mut(i)
let d: mut Derivation = ...;

if is_foo(t) {
  t.text += "foo";
  t.metadata = ...;
  d.update_history()
}

if is_bar(t) {
  t.text += "bar";
  t.metadata = ...;
  d.update_history()
}

...

Or ideally I could use something like this to save boilerplate, since I'm implementing hundreds of these rules and they have some common structure:

if is_foo(t) {
  change_text_and_update_history(d, t, "foo");
}
if is_bar(t) {
  change_text_and_update_history(d, t, "bar");
}

As you might have guessed, the issue I'm having is that both t and d are mutable references to the same underlying data. I've tried to work around this with something like:

if let Some(t) = d.get_mut(i) {
  if is_bar(t) {
    t.text = ...;
    t.metadata = ...;
    d.update_history()
  }
}

But this is rather verbose. As I mentioned above, I'll have to implement hundreds of these if is_foo checks, and I want the syntax of these checks to be as terse as possible.

I'm enough of a novice that the Rusty way to implement this is not obvious to me. Thanks in advance for any help you can provide.

This is the right idea, but in order to get the borrowing to work out, you'll need to make it an operation on Derivation that doesn't take a reference to the Term, but an index:

impl Derivation {
    pub fn maybe_update_term(
        &mut self,
        index: usize,
        predicate: impl Fn(&Term) -> bool,
        updater: impl Fn(&mut Term),
    ) {
        let term = &mut self.terms[index];
        if predicate(term) {
            updater(term);
        } else {
            // Don't update_history() since no change was made.
            return;
        }
        // At this point the `term` borrow will not conflict since it is not in use.
        self.update_history();
    }
}
Compilable sample code
use std::collections::HashSet;

struct Term {
    text: String,
    metadata: HashSet<String>,
}

#[derive(Default)]
struct Derivation {
    terms: Vec<Term>,
    history: Vec<String>,
}

impl Derivation {
    pub fn update_history(&mut self) {}

    pub fn maybe_update_term(
        &mut self,
        index: usize,
        predicate: impl Fn(&Term) -> bool,
        updater: impl Fn(&mut Term),
    ) {
        let term = &mut self.terms[index];
        if predicate(term) {
            updater(term);
        } else {
            // Don't update_history() since no change was made.
            return;
        }
        // At this point the `term` borrow will not conflict since it is not in use.
        self.update_history();
    }
}

fn is_foo(term: &Term) -> bool {
    false
}

fn main() {
    let mut d = Derivation::default();

    d.maybe_update_term(0, is_foo, |t| {
        t.text += "foo";
    });
}

It's not necessary to take two separate functions; there could also be an updater that returns whether it made any changes. But having a predicate that is not allowed to mutate is less error-prone.

1 Like

Thank you! So far, this approach is working very well for my use case.

If it's all right to ask a follow-up question here, I'm curious if you have suggestions for how to handle a slightly more complicated control flow that corresponds to something like:

if is_foo(t) {
  ...
} else if is_bar(t) {
  ...
} else if is_baz(t) {
  ...
}

My first instinct was to update maybe_update_term to return a bool if the rule applied. A naive approach works but is clumsy:

let ok = d.maybe_update_term(..., |t| t.text += "foo");
if !ok {  
  let ok = d.maybe_update_term(..., |t| t.text += "bar");
  if !ok {
    ..
  }
}

But writing that makes me sad. I thought instead that it would be more elegant to model this as a list of closures with the goal of doing something like:

let rules = [
  || d.maybe_update_term(..., |t| t.text += "foo"),
  || d.maybe_update_term(..., |t| t.text += "bar"),
  ...
];
for rule in rules {
  if rule() {
    break;
  }
}

But as you might have guessed, I'm having some issues with closure type inference here. I think I'm close to representing this logic simply and clearly, but for now I'm sadly just kludging it everywhere. Do you have any advice on how to represent this?

There's no excellent syntactic solution for that problem. With the boolean return value you can use short-circuiting logical or to do it:

d.maybe_update_term(...)
    || d.maybe_update_term(...)
    || d.maybe_update_term(...);

It's not great to be using that for its side-effects, but at least it's clear enough what it must be doing in this case.

1 Like