cursive icon indicating copy to clipboard operation
cursive copied to clipboard

[FEATURE] Strongly-typed user data (or: implementing generic traits for Cursive based on user data)

Open schneiderfelipe opened this issue 2 years ago • 2 comments

Hi @gyscos! I would like thank you for this nice library and ask you a question regarding extending cursive for more structured data/state management.

It seems that user data is typed as Any, is that right? Could it be possible to support some sort of strong typing? That would allow one to trivially implement new traits for Cursive by leveraging the internal data (by delegating methods to the internal object, I'm specifically trying to implement the Redux pattern as a trait). I'm not sure of this is possible without the compiler knowing the data type.

One could always default to a boxed Any or something like that, so that wouldn't require breaking things. What do you think about that?

schneiderfelipe avatar Aug 09 '21 16:08 schneiderfelipe

Hi, and thanks for the report!

By strongly typed user data, do you mean making Cursive generic on the user data type? I'm afraid this would lead to a generic parameter virtually everywhere, and would make type inference on some lambdas a bit more tricky.

Rather, you should be able to downcast the user data as desired. And implementing new traits on the Cursive object should be just as doable.

Do you have a specific example in mind where strongly typed user data would help?

gyscos avatar Aug 09 '21 18:08 gyscos

Hi, and thanks for the report!

Happy to do so!

By strongly typed user data, do you mean making Cursive generic on the user data type? I'm afraid this would lead to a generic parameter virtually everywhere, and would make type inference on some lambdas a bit more tricky.

Yes, that's basically what I mean. I could try a PR and check if it's doable, what do you think? (I don't want to mess up with simple use cases, of course. So if it gets too tricky, i.e., if the user has to add types to everything, then I would rather not have generics as well.)

Rather, you should be able to downcast the user data as desired. And implementing new traits on the Cursive object should be just as doable.

Yes, and that works like a charm 🎉, but it's hard (impossible?) to implement traits that are generic on user data.

Do you have a specific example in mind where strongly typed user data would help?

I made a (rather long) gist to show what I mean (it basically implements a (poor man's) reactive counter using the Redux pattern), I hope it's not too boring! Below is some code from there (links to the lines are in the comments).

The most important part (dispatching to the user data) is

// ...
// https://gist.github.com/schneiderfelipe/f05781cabbea05b85d5badd526fefd1d#file-take2-rs-L39-L70
    siv.add_layer(
        Dialog::around(TextView::new("Hello, world!").with_name("content"))
            .button("+1", |s| {
                let mut view = s
                    .find_name::<TextView>("content")
                    .expect("`content` not defined");
                s.with_user_data(|store: &mut MyStore| {
                    // Now we also update the text with the counter.
                    store.dispatch(Message::Increment);
                    // The below does not work (`error[E0501]: cannot borrow
                    // `*s` as mutable because previous closure requires
                    // unique access`), so we I have to use
                    // `s.find_name("content")` outside of the callback.
                    //
                    // s.call_on_name("content", |view: &mut TextView| {
                    //     view.set_content(format!("{}", store.counter))
                    // })
                    view.set_content(format!("{}", store.counter));
                });
            })
            .button("-1", |s| {
                // Lots of code duplication here.
                let mut view = s
                    .find_name::<TextView>("content")
                    .expect("`content` not defined");
                s.with_user_data(|store: &mut MyStore| {
                    store.dispatch(Message::Decrement);
                    view.set_content(format!("{}", store.counter));
                });
            })
            .button("Quit", |s| s.quit()),
    );
// ...

Lots of duplication, right? So I started dispatching on the Cursive object (by implementing the Store trait, basically delegating to the user data):

// ...
// https://gist.github.com/schneiderfelipe/f05781cabbea05b85d5badd526fefd1d#file-take3-rs-L37-L73

// Almost there: wrap `MyStore` into `Cursive` by delegating to `user_data()`.
impl Store<i32, Message> for Cursive {
    fn dispatch(&mut self, action: Message) {
        // Way less code duplication here, but `view` still has to be obtained
        // outside the closure. For a more complex example (with many views
        // that could change), this could not be very ergonomic, but I don't
        // know any other way to do it.
        // EDIT: I actually know: by using subscriptions! 🎈
        let mut view = self
            .find_name::<TextView>("content")
            .expect("`content` not defined");

        self.with_user_data(|inner_store: &mut MyStore| {
            inner_store.dispatch(action);
            view.set_content(format!("{}", inner_store.counter));
        });
    }
    // Has to be `&mut self` because `with_user_data` requires so!
    fn get_state(&mut self) -> i32 {
        self.with_user_data(|content: &mut MyStore| content.get_state())
            .expect("user data not initialized")
    }
}

// ...

    siv.add_layer(
        Dialog::around(TextView::new("Hello, world!").with_name("content"))
            // Much cleaner code 🎉, we are dispatching directly to `siv`, all
            // the business logic is in the `Store` implementation.
            .button("+1", |s| s.dispatch(Message::Increment))
            .button("-1", |s| s.dispatch(Message::Decrement))
            .button("Quit", |s| s.quit()),
    );
// ...

Apart from the minor issue of having to give a &mut Cursive to get_state (I should probably avoid with_user_data there, but anyway) and not having any subscription (not the point here), this code is much cleaner. But it is also very generic: I don't need any information on the user_data, so this issue is all about making the following possible:

// Not in the referenced gist

// Ultimate goal:
impl<UserData, State, Action> Store<State, Action> for Cursive<UserData>
where
    UserData: Store<State, Action>,
{
    fn dispatch(&mut self, action: Action) {
        // Pretend we have proper subscriptions to the store, so we don't need
        // to modify the UI here.
        self.with_user_data(|user_data: &mut UserData| {
            user_data.dispatch(action);
        });
    }
    // Even `user_data` requires `&mut self`. That's OK, I can see why, but
    // it would be nice to have an alternative that returns `Option<&T>`
    // instead of `Option<&mut T>`.
    fn get_state(&mut self) -> State {
        self.user_data::<UserData>()
            .expect("user data not initialized")
            .get_state()
    }

    // other methods...
}

The above is data-agnostic and can be generalized to any trait: as long as UserData implements SomeTrait, one can write a trivial implementation of SomeTrait for Cursive by just delegating all methods to UserData. But, correct me if I'm wrong, this (general setting) can only work if Cursive is generic on UserData. And UserData = Box<dyn Any> could be given by default by the general interface (cursive::default(), etc.).

(And sorry for the wall of text! 😄)

schneiderfelipe avatar Aug 09 '21 22:08 schneiderfelipe