Closed MartinKavik closed 4 years ago
isolate all the side effects inside of run instead of distributing the stateful effects across
finish
andrun
There are more hidden side-effects in AppBuilder
's methods - see e.g. mount
.
App::build(update, view).finish().run()
App::build(update, view).run()
would be even nicer.::default()
function, because implementation of Default
trait for Model
is optional. It's a bit of a departure from what we currently have, but I think that it makes the implementation a little less weird and the usage clearer about when exactly the InitFn happens (since it seems to run half in finish and half in run right now).
I agree. I've added init
as the part of the bigger PR (https://github.com/David-OConnor/seed/pull/189). AppBuilder
code wasn't super clean before this change and I've made it worse as a trade-off for fewer breaking changes.
mount it to a different place depending on the initial route
Interesting. Do you have a real-world use-case?
So I basically agree that builder API can be improved and I've created this issue so we can design it properly. Implementation details can be discussed in original PR (https://github.com/David-OConnor/seed/pull/235).
Inspiration from other frameworks:
There are more hidden side-effects in
AppBuilder
's methods - see e.g.mount
.
I don't think it modifies the DOM, so I didn't consider it a side effect. By side effect, I meant that it doesn't modify any global state (i.e. the DOM). It may panic, but I don't consider that a side effect either.
App::build(update, view).run()
would be even nicer.
It shouldn't be difficult to collapse finish
and run
. At the same time, I'm a bit leery to combine the build and run capabilities. If we did implement this, it would look like this:
fn run(self) {
self.finish().run()
}
so it shouldn't be a big deal.
I'm not sure if compiler allows you to write
::default()
function, because implementation ofDefault
trait forModel
is optional.
I have, in fact, implemented Init::default
here. I don't think it's a breaking change.
Interesting. Do you have a real-world use-case [for multiple mount points]?
Say that you had a relatively large (mostly static) webpage that had multiple feed
s in scattered locations throughout your page. I was thinking that it might be easier to provide a Model
with a url for what the feed was listening to and mount two instances of the App
with different urls in two different locations. We could simply get around the thing by instantiating "pages" or subsections, but I think that running multiple instances is the way to go as there's not much point in them being connected since the two parts are entirely disjoint.
It may panic, but I don't consider that a side effect either.
It is a problem, because it's surprising. You think that you are only configuring your application but it is calling DOM and can panic under your hands.
I have, in fact, implemented Init::default here. I don't think it's a breaking change.
I tried it and you are right, good.
Say that you had a relatively large (mostly static) webpage that had multiple feeds in scattered locations
Nice example. Have you tried it?
Hmm, got it about the about the panic. Well, if we allow for mounting different copies of the App
in different places, this would need to go into the Init
struct, so I guess it depends on what happens there, I suppose.
Have you tried it?
For the example: no, I have not. I was just exploring the issue mentally. I haven't had a lot of time to code for the past week-ish.
Hm, it seems that run
is a one-shot method, so I would also suggesting creating a third class between AppBuilder
and App
that only has the method run
if we continue with the build
-> finish
-> run
pattern.
Can we make it simpler? I.e. two classes - App
and AppBuilder
and start app with App::build(update, view).run()
?
Hence the "if".
Again, however, this version is simpler but the other is more flexible, so we can always hide the above call with a run
implemented on the AppBuilder
. In fact, we would simply hide all of the calls with App::run(update, view)
if we take it to an extreme.
Let's write a list of options so we can move forward:
1) App::run(update, view)
routes
2) App::build(update, view).finish().run()
finish
3) App::build(update, view).run()
4) App::build()
.update(update)
.view(view)
.run()
App::build()
Can you complete the list (add more variants, add - or +, etc.)?
I want to note that this isn't necessarily a list of exclusive options since each option is a strict superset of what's allowed by the previous option apart from 3. We could implement all of these functions and link them to each other in the documentation.
Namely,
App::run(update, view)
is equivalent to
App::build(update, view).run()
with the following impl
impl App {
fn run(update: _, view: _) {
App::build(update, view).run()
}
}
App::build(update, view).run()
is equivalent to
App::build(update, view).finish().run()
with the following impl
impl App {
fn build(update, view) -> AppBuilder {
AppBuilder::new(update, view)
}
}
// -- snip
impl AppBuilder {
fn finish(self) -> App {
App::new(...)
}
fn run(self) {
self.finish().run()
}
}
I don't like this, but
App::build(update, view).finish().run()
is equivalent to
App::builder()
.update(update)
.view(view)
.run()
with the following impl
impl App {
fn build(update, view) -> AppBuilder {
App::builder()
.update(update)
.view(view)
}
}
// -- snip
impl AppBuilder {
fn build(self) -> App {
App::new(...)
}
fn run(self) {
self.build().run()
}
}
I'm going to name each of those:
App::run(update, view)
single function run
sink
s as they have no need for routes
.App::build(update, view).finish().run()
build then run
build
and run
, but not useful otherwise.build
doesn't actually build, but starts the building processfinish
finishes the building process, but also partially starts running the App
run
behaves as expectedApp
and running the App
This potentially aids in disallowing illegal configurations.finish
take anything as an argument.App::build(update, view).run()
build and run
build
and run
, but not useful otherwise.App
-- I need to pass around an AppBuilder
if I want to start running the App
at a later time (i.e. initial http(REST) api calls to get state)App::build().update(_).view(_).run()
spread builder
Note: I don't like the name of the function build
since we aren't building the app, but rather creating a builder. Actually, the typical names I've seen for the Builder pattern is builder
then build
instead of build
then finish
. Examples: typed-builder and derive_builder
Also, I would like to clarify: what about the originally suggested change, where run
takes a lambda to create the Init
struct instead of it being passed into the build
function? Is that accepted, or still under discussion?
I love the short API App::run(update, view)
, with the ability to expand for flexibility, as-required.
App::builder(update, view).build().run()
App::run(update, view)
App::run(update, view)
and App::run(self)
)run takes a lambda to create the Init struct instead of it being passed into the build function
- Could you provide some real-world examples where it's better for Seed users or where we can't use builder's
init
?
I agree with the points on 1.
With regards to App::run(update, view)
, if you don't need sink
, or routes
(aka it's not a single page app), and you don't do anything fancy with the mount or the window, this is sufficient.
As for the run
duplication issue, we can just come up with a different name. If we go with a three struct sequence (something like AppBuilder
-> AppRunner
-> App
), this shouldn't be a concern as run is implemented on AppRunner
and App
separately.
As for init
, again, the problem is how we need to implement build
(or finish
as it is called). The init function is not run when the app is started, but rather on calling build
which is very unexpected. Not to mention that the App
's state is inconsistent with when update
or view
is called. This is the primary concern I have for sticking the init function into the AppBuilder
. It's not used anywhere other than in the run
function, so I don't think it's necessary to initialize it in the builder. Not to mention that we can then remove two fields in AppCfg
that are never used again after run
. It's just that I want to isolate the state to where it's being used instead of passing it through.
I ran into this because I had assumed that the init
function would get called prior to anything else the App
did, but I ended up having to chain a future outside of Seed and initialize init
due to various interactions between my update
, run
, and init
functions.
App::run(update, view)
As for init, again, the problem is how we need to implement build
@David-OConnor @flosse could you share your opinions, please?
@MartinKavik I had not time to read all the comments.
- I don't care about implementation now if it looks at least doable. I'm trying to find best design (i.e. find real-world examples) from the user point of view, we can discuss implementation in PR.
I totally agree!
And I'd split the API at least into a builder and a final App
.
Moreover I don't like the name init
because I have no idea what it means.
I'd prefer a more precise naming like before_mounted
or after_mounted
etc.
Without really thinking about all the point, I'd draft the API like this:
App::new(update, view) // create a builder
.routes(my_routes) // configure whatever you like
.before_mounted(before_mnt) // declare what should happen but don't do it now
.after_mounted(after_mnt) // again, declare but execute it later
.build() // build the instance
.run(); // run it
More drafts to discuss.
Minimal example:
App::builder(update, view).build_and_run();
Example with all builder methods:
App::builder(update, view)
.routes(routes)
.window_events(window_events)
.sink(sink)
.before_mount(before_mount)
.after_mount(after_mount)
.build_and_run();
fn before_mount(_url: Url) -> BeforeMount {
BeforeMount::new()
.mount_type(MountType::Overtake)
.mount_point("application")
}
fn after_mount(_url: Url, _orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
AfterMount::new()
.model(Model { count: 5 })
.url_handling(UrlHandling::None)
}
Changes:
::builder
and .build
_and_run to use more standard naming as @AlterionX suggested.build_and_run
to prevent using App
in non-initialized state (e.g. it panics on app.update(..)
).before_mount
and after_mount
to better express purpose as @flosse suggested.before_mount
is a combination of .mount
and .mount_type
so we can set it when the app is started as @AlterionX suggested.after_mount
is renamed init
.I like this one. The before_mount
and after_mount
is super nice, imo.
We could attempt leaving in the older methods, but just deprecate the ones that are no longer useful as well.
@David-OConnor Is that API design ok with you?
@David-OConnor: I'm looking at the builder pattern as an implementation detail that allows some details (routes, before_mount etc) to be optional, and start (or something else) is simple, vice a formal pattern. Ie someone not familiar with the builder pattern may be confused (What does build vs builder vs run do?), when the purpose of this code is mainly to specify which init, update, and view fns to use. (While letting you include optional ones) (https://github.com/David-OConnor/seed/pull/275#issuecomment-549037009)
I don't think that the builder pattern is an implementation detail in this case because it's the part of public API. When I open AppBuilder
docs and see method start
I would be probably confused - does it mean start building or start App
?
start
, build_and_run
, run
..
Another alternative could be start_app
.
Other ideas/opinions please (see previous comment)? @flosse , @AlterionX
Seems like we've already settled on build_and_run
? A bit posthumous, but personally a fan since it doesn't stray from my knowledge of builders.
@David-OConnor Do you have more input regarding the API? Or is this okay?
@AlterionX final version is https://github.com/David-OConnor/seed/blob/5ce6379c9affc3c65d204c91221f20f08f7b6069/src/vdom/builder.rs#L220
Oh, my bad. Read that wrong.
Don't we still have before_mount
and after_mount
though?
Yep - those aren't implemented, but look good. I have no additions.
I think we can close this. All the changes have been implemented.
@MartinKavik @David-OConnor I'm starting to get half a mind to move the
Init
frombuild
torun
or something similar to isolate all the side effects inside ofrun
instead of distributing the stateful effects acrossfinish
andrun
.I think that this is better since we do this weird dance around
initial_orders
right now where we don't quite have the correct state to do what we have half the state we want insideBuilder
and half the state inside theApp
.So we would have
or
It's a bit of a departure from what we currently have, but I think that it makes the implementation a little less weird and the usage clearer about when exactly the
InitFn
happens (since it seems to run half infinish
and half inrun
right now).Personally, I also think that
mount
should also be insideInit
since we might want to mount it to a different place depending on the initial route, but we seem to keep that around forever, so I changed my mind. Point is, there are parts ofAppCfg
that are not immutable, and I would like to change that.The downsides, I think, are also fairly obvious in that it's a breaking change, and so on. Also, the model is now provided in the
run
method instead of thebuild
method, which is also... unique, so to speak.I haven't seen if the types work out yet, but it seems reasonable to me.
Originally posted by @AlterionX in https://github.com/David-OConnor/seed/pull/235#issuecomment-542944604