Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why Seed doesn't use a traits for the models #310

Closed
MuhannadAlrusayni opened this issue Dec 10, 2019 · 5 comments
Closed

Why Seed doesn't use a traits for the models #310

MuhannadAlrusayni opened this issue Dec 10, 2019 · 5 comments

Comments

@MuhannadAlrusayni
Copy link
Contributor

Hi, Why Seed doesn't use a traits for the models ?

In another way, why Seed doesn't have trait(s) that every model must implement? I think this would simplify the way we create and use models..

First, I thought that Seed uses functions for the flexibility so that every model chose how to call it's view and update method, thus Seed doesn't restrict the model API.

But I have tried to use traits to see if there are any blocking limitation that would block Seed from using traits and I didn't find any of that. I actually found it more convenient to call model.view() or model.update(msg, orders) for any model so I don't need to know how construct the model or what args the view method needed to view the model (these should be handled else where, "decoupling").

Here is the traits I use:

pub trait View<Msg: 'static> {
    fn view(&self) -> Vec<Node<Msg>>;
}

pub trait Model<Msg, GMsg>: View<Msg>
where
    Msg: 'static,
    GMsg: 'static,
{
    fn update(&mut self, _: Msg, _: &mut impl Orders<Msg, GMsg>);
    fn sink(&mut self, _: GMsg, _: &mut impl Orders<Msg, GMsg>) {}
}

We can write bounds for view/models this way:

fn view_debug_mode<Msg>(item: &impl View<Msg>) -> Vec<Node<Msg>> {
    item.view()
        .into_iter()
        .map(|node| unimplemented!())
        .collect()
}

Using traits makes writing macros for Model/Views easier task, since macros would easily call view() or update() following the trait functions signatures.

It would also make App and AppBuilder API more friendly and simple:

    App::builder::<MyApp>()
        // The remaining functions can be converted to App trait too!
        .before_mount(before_mount)
        .after_mount(after_mount)
        .routes(routes)
        .window_events(window_events)
        .build_and_start();

Traits would be good to have if we want to make reusable View/Models libraries, so that these libraries have a unified API to update or view models.

Traits with a simple functions would make View/Models easy to learn and use.

Some other frameworks do have trait that does similar thing (e.g. Component, Model ..etc)

I am not sure if there is a disadvantage of using traits, the only thing I can see is that trait functions can't return impl View, but that is not really a blocker.

This is open talk, share your thoughts. :D

@MartinKavik
Copy link
Member

My opinion: Model isn't a basic part of apps / components / modules, but necessary evil.

You have to define your Model when you are creating a new Seed app because I think you'll need to use, at least a minimal, state in 99% of apps. So make it optional isn't a good trade-off for added API & code complexity and confusion for beginners.
=> Required Model in App is a special case.

When we would force app developers to use Model as the basic entity:

  • It's a signal for them to use Model (aka state) more - they are used to create components / objects, so it's natural to them. We would see projects where the state is scattered all over the application and communication among components are hard to track.
    => One source of truth and a tree structure is gone.
  • They would want / have to use methods defined in Model's traits.
    • They wouldn't write specialized (=simple and shorter) components but rather a bigger general ones with a state and update, even if only view would be enough.
    • Components would be less explicit. For example - one of my components has empty Model and update is doing nothing. But I don't know it on the first look, so I still would try to write "plumbing" to call update.
      => More code, more potential bugs
    • It's often useful to write custom signature for e.g. update - it would be practically impossible. See this example.

How I'm writing Seed apps from scratch:

  1. I write a basic skeleton in lib.rs (view, update Model, start).
  2. Continue to write code in lib.rs.
  3. Once the view function is too big, I divide it into multiple shorter ones with names view_something.
  4. I move them into new files once the lib.rs is unreadable.
  5. Once I find out that some view_* functions are almost the same / identical (2 cases should be enough but I recommend to wait for 3 or more almost the same / identical functions), I refactor them to one view_* function. It's basically a super simple component (= submodule).
  6. Once I find out that there are many non-business Model properties like dropdown_opened and my code becomes harder to read because of them, I try to move those properties "down", near the place where they are used - i.e. near the view_* function that uses them. Now we have a "classic" component with a view function and a Model that contains some properties.
    => I constantly switching between writing and refactoring and components with a state are rather a side-effect than a first-class construct.
    However I agree that creating a library is a bit harder because of that, but on the other hand you know that components in a library contain only really essential parts and nothing more.

My background, so you know why I think like that:
I was working as a frontend / fullstack dev in the age of jQuery and AngularJS, then I moved to backend (monoliths and then microservices) and then back to frontend through Elm and Seed (and a little bit Vue + React). So I know that microservices / components / actors can be very useful, but it's also very difficult to do them right - to make them easy to reason about and easy to debug.


@MuhannadAlrusayni I've written my reasoning behind that design decision, but I'm happy that you want to break status quo. Maybe you are right and it would make Seed apps simpler and less error-prone.

If we want to continue with the discussion, I suggest to create a bigger example with nested components so we can compare current and proposed API and architecture properly. Then, if that example would look promising, we would need to rewrite at least real-world example and quickstart-with-webpack. After that there will be a chance to merge it into the master because it would be a really big change.


Other opinions or ideas?

@MuhannadAlrusayni
Copy link
Contributor Author

MuhannadAlrusayni commented Dec 11, 2019

  • It's a signal for them to use Model (aka state) more - they are used to create components / objects, so it's natural to them. We would see projects where the state is scattered all over the application and communication among components are hard to track.
    => One source of truth and a tree structure is gone.

  • They would want / have to use methods defined in Model's traits.

    • They wouldn't write specialized (=simple and shorter) components but rather a bigger general ones with a state and update, even if only view would be enough.

Well, this depends on the developer, even in the current Seed implementation that can happen.

  • Components would be less explicit. For example - one of my components has empty Model and update is doing nothing. But I don't know it on the first look, so I still would try to write "plumbing" to call update.
    => More code, more potential bugs

This issue would arise if we have one trait say Component that have both view() and update() as its functions.

But with the traits I mentioned earlier (View and Model), these components would implement View trait only, and calling update() is compile time error.

  • It's often useful to write custom signature for e.g. update - it would be practically impossible.

This might be true for app developer's point of view.

But from library developer point of view, they would have hard time to make a unified API, and would be impossible to use 3rd-partiy libraries unless they hard code them. But using traits would make our code works with any type that implement these traits, even types that don't exist yet!.

See this example.

I tried to understand your example, sorry but I couldn't understand how things work in it :D, it would be cool if you make simplified version of the things you're trying to show me.

How I'm writing Seed apps from scratch:

  1. I write a basic skeleton in lib.rs (view, update Model, start).

  2. Continue to write code in lib.rs.

  3. Once the view function is too big, I divide it into multiple shorter ones with names view_something.

  4. I move them into new files once the lib.rs is unreadable.

  5. Once I find out that some view_* functions are almost the same / identical (2 cases should be enough but I recommend to wait for 3 or more almost the same / identical functions), I refactor them to one view_* function. It's basically a super simple component (= submodule).

  6. Once I find out that there are many non-business Model properties like dropdown_opened and my code becomes harder to read because of them, I try to move those properties "down", near the place where they are used - i.e. near the view_* function that uses them. Now we have a "classic" component with a view function and a Model that contains some properties.

This workflow is doable with traits approach too.

=> I constantly switching between writing and refactoring and components with a state are rather a side-effect than a first-class construct.
However I agree that creating a library is a bit harder because of that, but on the other hand you know that components in a library contain only really essential parts and nothing more.

I agree that not every object should be component/model, some are View only and some
are View and Model, View are these object that doesn't need to have state,
while Model are these object that need to have their own state.

If we want to continue with the discussion, I suggest to create a bigger example with nested components so we can compare current and proposed API and architecture properly. Then, if that example would look promising, we would need to rewrite at least real-world example and quickstart-with-webpack. After that there will be a chance to merge it into the master because it would be a really big change.

Sure I was experimenting with these two traits in the past 4-5 weeks, and I
started a new library that adapt these traits, will make the library public when
I write the basic Model/Views with some examples, but this will take time :D.

@MartinKavik
Copy link
Member

MartinKavik commented Dec 12, 2019

This might be true for app developer's point of view.
But from library developer point of view, they would have hard time to make a unified API, and would be impossible to use 3rd-partiy libraries unless they hard code them.
...
I started a new library that adapt these traits..

It looks like the real question isn't "Why Seed doesn't use traits for the models?" but "How to write a library for Seed / in Elm architecture?".
So I suggest to zoom out and focus on the problem you are trying to solve instead of the one of the potential solutions. Or am I wrong and you want to resolve other pain point(s)?

@MuhannadAlrusayni
Copy link
Contributor Author

Yeah, you're right, I found that using traits is one of the simplest ways to write libraries for Seed, that's why I asked.

@MartinKavik
Copy link
Member

@MuhannadAlrusayni see #111
If you think that it wouldn't make sense to continue in that issue, please create a new one like "How to write a library for Seed?".

I'm closing this issue because I hope I answered the original question and we can reopen it once something important (e.g. Seed API) is changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants