Shared state: How to port JS / MVC code to rust?

I have a JS project using a MVC pattern that I would like to port to rust. I tried to simulate the JS flow as best as possible in rust (without use of lifetimes/ refcell/ rc/ arc/ mutex/ etc.) in this playground: MVC Sample

It's currently broken because ctrl.model.prop is still foo (because I only modified a clone of the model).

What would be the best route to take to refactor this so that the updated model is available to future event handlers?

I'm still new to rust so the The (refcell/ rc/ arc/ mutex) stuff is a little hard to grok at the moment (and may introduce other issues?), but it seems like adding those wrappers is the only way?

Some things I have looked at that helped me with creating the MVC sample:

Thanks in advance for your time.

I'm confused about what you're trying to do, probably because I don't know the original JS/MVC approach. In this code:

    let ctrl = Controller::new();
    let mut ctrl_a = ctrl.clone();
    let ctrl_b = ctrl.clone();

    /*
    console_log!("--> Before: ctrl.model.prop = {}", ctrl.model.prop);
     */
    println!("--> Before: ctrl.model.prop = {}", ctrl.model.prop);

    ctrl_a.hendle_event_01(1, None, ctrl_b);

Do you really only want one controller, but you had to clone it (twice) to get it to compile?

If so, why does hendle_event_01 have two controller params: self and ctrl? Seems like you could get rid of the ctrl param and perhaps that will allow removing at least one of the clone calls.

If you can't clone without causing a logic error (the one you mentioned), then remove the clone and show us the errors you're getting. This will lead to better answers.

In general, I suspect that instead of bundling up structs like this,

struct Controller {
    model: Model,
    view: View,
}

you'll need to pass the Model and View separately as params. When you bundle them like this, and you have a method such as Controller::hendle_event_01(&mut self, ...), all the fields of Controller (model and view) are borrowed mutably, which means borrowed exclusively. This will cause conflicts when you try to use these fields in other ways at the same time. A mutable/exclusive borrow will prevent any other use of the fields.

Yes, the double-clone in the main function was to get it to compile. I think I tried a version with passing in a Model and View struct but didn't get very far (because of the borrowing). I can try to redo it again that way and post a new playground link.

Edit: Actually, I think it was because the separately created structs would not have references back to the controller struct. If I were to put references in both (e.g. controller to view, view to controller), then I get a circular reference error.

Also in the JS version, the args parameter would be an object containing a set of properties that are relevant to the model or view (e.g. event XHR response, current data filter in use). The ctrl parameter would be an object containing a bunch of callbacks line: \\ (do stuff) relevant to the view (e.g. modify the DOM, attach some event listeners). In the linked playground, I only have one property and one callback.

Right, references in Rust aren't meant to be used that way.

You may not be able to "port" what you were doing in JS to Rust, and in fact this is almost always a bad idea. Instead you'll need to learn Rust to some degree as that link recommends (if you haven't already), then try to implement what you want in Rust (as opposed to porting JS code). People here can help you at the point you're trying something specific and you're getting compiler errors, i.e., a step at a time.

If you're wanting someone to just show you how to do MVC in the browser using Rust, I won't be able to help but maybe others can.

If you need back-references of any kind, you will probably need something like Rc, Arc, etc. The only other way to have back-references is to store them as a key to another data structure -- an index in a array or Vec, a key to a HashMap, etc. And even then you may need Rc or Arc to reference the shared data structure.

So in Rust the preferred design choice is to avoid back references. But if that's not practical, then Rc, Arc, etc. are often needed.

2 Likes

While researching how to avoid back-references. I came across this article:

Which was a good refresher for me (Try to do as much as possible without borrowing or cloning. Return discrete standalone data).

So I did two more rewrites which pretty much satisfies what I was looking for:

  • MVC Sample #01 - Broken. undefined does not change to foo.

  • MVC Sample #02 - Working. undefined changes to foo - by saving each stage to a variable before passing to the next stage. But all the values/callbacks come from the Controller. In practice, the values/callbacks would come from the Model and View.

  • MVC Sample #03 - Working. Model and View can now signal the Controller to go to the next step/stage. Method: Mediator Pattern. In short:

    • Add a Mediator trait to the Controller. That trait (method) will serve as the callback.
    • Add a Mediator property to the Model and View (requires lifetime annotations).
    • When the Model and View are being created, pass in the Controller which contains the Mediator implementation.

    Now when the Model or View calls its Mediator method, a method in the Controller will be called. No refcell, rc, arc, mutex etc. required.

I think it's good you're using this pattern, as it seems to apply to your situation.

But I don't understand how this would work in a more real world example, where there would be an event loop of some kind that expects these objects to stay alive. For example, several Controller objects are created on the stack (&Controller {} ) and these will be dropped when Controller::init returns. This is why no Rc is required.

I'm probably not understanding how this would actually be used in the browser, but I do suggest trying to make it work there, with some sort of small but realistic test case, before concluding that this approach will actually work.