Closure passed to function doesn't update local hashmap

Hi, I'm currently trying to implement a Jupyter Notebook-like editor for a small programming language i'm working on.
I'm using socketioxide, axum, and tokio to set up a server that can receive connections coming from the frontend of this project (made with React).

Every time a new client connects, an on_connect function should run and should then wait for events from the client with the event name of "run". The first thing on_connect does is create a hashmap that stores a user's environment for any variables they declare. if there is an assignment, like let a = 1 2 ; 3 4, it will store "a" in the hashmap with the corresponding variable.

async fn on_connect(socket: SocketRef) {
    info!("socket connected: {}{}{}", BLUE, socket.id, DFLT);
    let mut env: HashMap<String, LalaType> = HashMap::new();

    socket.on("run", move |s: SocketRef, Data::<Cell>(data)| {
        info!("Received message from {}{}{}: {:?}", BLUE, s.id, DFLT, data);

        let input = data.cell_text.trim();

        let _ = data.auth.trim();

        let ast = parser::parse(input).unwrap();
        
        info!("env before interpreting: {:?}", env);

        let response = interp(&ast, Some(&mut env), true).unwrap();

        info!("env after interpreting: {:?}", env);

        let output = CellOutput {
            output: response
        };

        info!("Sending message to {}{}{}: {:?}", BLUE, s.id, DFLT, output);

        let _ = s.emit("output", output);
    });
}

you can see the full file here.

the problem is that the env hashmap does not persist across messages. so if i send a message like this:
run {cell_text: "let a = 1 2 ; 3 4", auth: "123"} it will store it in the hashmap properly, and i can see the env is updated by my info! print under the declaration of response. however, if i then send a new message: run {cell_text: "a", auth: "123"} it will show that the hashmap is empty before interpreting.

example output:

2024-04-28T21:03:29.485458Z  INFO lala_bin: socket connected: PjUxAlEmQJgCQUlD
2024-04-28T21:03:30.934609Z  INFO lala_bin: Received message from PjUxAlEmQJgCQUlD: Cell { auth: "123", cell_text: "let a = 1 2 3 ; 4 5 6 ; 7 8 9" }
2024-04-28T21:03:30.934766Z  INFO lala_bin: env before interpreting: {}
2024-04-28T21:03:30.934792Z  INFO lala_bin: env after interpreting: {"a": Matrix(Matrix { rows: 3, cols: 3, data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] })}
2024-04-28T21:03:30.934805Z  INFO lala_bin: Sending message to PjUxAlEmQJgCQUlD: CellOutput { output: "[1.00 2.00 3.00]\n[4.00 5.00 6.00]\n[7.00 8.00 9.00]\n" }
2024-04-28T21:03:32.069759Z  INFO lala_bin: Received message from PjUxAlEmQJgCQUlD: Cell { auth: "123", cell_text: "a" }
2024-04-28T21:03:32.069869Z  INFO lala_bin: env before interpreting: {}
thread 'tokio-runtime-worker' panicked at src/interp.rs:176:60:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

i've done some experimenting and even just declaring an i32 before socket.on() and incrementing it in the closure doesn't seem to make the variable's new value stick. how can i make the closure update my hashmap and have these changes be reflected outside of the closure?

You are moving the hashmap into the closure and presumably cloning it on each event. You probably want to wrap it inside an Arc<Mutex<>>.

1 Like

I don't think that's it - the hash map is moved once into the closure, which is then called repeatedly by the socket. And the socket cannot make more copies of the closure (and with it the hash map) or other such shenanigans because there's no Clone bound anywhere to be seen.Socket::on takes a MessageHandler which is just Send + Sync.

The Clone bound is on the impl of the trait for closures, see here under Implementors

1 Like

sorry, i havent really used Arc or Mutex in rust so can you explain why i'd need to wrap it inside an Arc<Mutex<>>? its been over a year since i read about them in the book too. i dont think two threads are ever trying to access the hashmap at the same time... i'd really like to learn more about why you suggested that, please. i will work on implementing it while i wait.

i implemented it and it worked!! thank you so much.

You can deduce from the implementations @yyogo linked...

F: FnOnce(..) + Clone + ...

...that if your closure is called more than once, it has been cloned. You can think of a capturing closure as a compiler generated struct that has fields for the captures and implements some of the Fn traits, and perhaps other traits like Clone as is possible.

struct Closure {
   _capture0: HashMap<String, LalaType>,
}

And every time the closure is cloned, it contains an independent clone of your HashMap.

You need shared ownership, where every clone of the closure contains the same HashMap. That's what Arc<_> provides. When you clone an Arc<_>, the original and the clone still reference the same data.

However, you can only get a &T out of an Arc<T> directly. But you need a &mut HashMap. And that's where the Mutex<_> comes in: It provides the synchronization needed to ensure there's only one &mut HashMap<_, _> at a time, even though you potentially have a bunch of Arc<Mutex<HashMap<_, _>>> existing at the same time.

5 Likes

i see. thank you for the explanation!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.