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

Store messages instead of model? #19

Open
rtfeldman opened this issue Dec 11, 2018 · 4 comments
Open

Store messages instead of model? #19

rtfeldman opened this issue Dec 11, 2018 · 4 comments

Comments

@rtfeldman
Copy link

rtfeldman commented Dec 11, 2018

Currently if I change init or my initial model, those changes will not get hot-loaded because hmr.js remembers the last model state.

I made a proof of concept back in 0.18 of doing hot loading by storing init and the list of messages instead of model: https://gist.github.com/rtfeldman/f259128af7fea653876c34cca033ae68

When you change your code, including init or the initial model, it would replay the messages on top of the new code. This would also continue to work for any of the existing use cases, because the current model state can be derived by replaying messages on top of the initial model.

Thoughts?

@klazuka
Copy link
Owner

klazuka commented Dec 11, 2018

Recently I've been discussing this with @opvasger who is exploring a pure-Elm solution that relies on the technique you described.

And I discussed it with @evancz during the 0.19 alpha because the introduction of Browser.Navigation.Key caused problems for the existing implementation. He recommended the same thing that you're talking about: keep track of every Msg that fires and replay them whenever you reload the code.

That idea has a lot going for it:

  • It is easier to implement and you don’t have to hook as many functions in the Elm runtime
  • It avoids the problems with Browser.Navigation.Key
  • As you mentioned, it allows hot reload to work when the model's type changes as well as when init functions change
  • It could be built to leverage the same code that the Elm debugger uses (if Elm were to introduce such an API)

But there are also some downsides:

  1. If you rename a Msg, it will playback the old message name which will silently no-op, which may result in a jarring app state.
  2. If you change how a Msg is handled by the update function, you can end up in a jarring app state.
  3. Memory allocations will grow indefinitely as you interact with the app.

What I mean by "jarring app state" in (1) and (2) is that when you reload the code, you expect that the visual state of the app remains the same with the exception of intentional changes that you made to how things are rendered. But imagine that you have a counter app and you clicked the plus button 3 times. Now you rename the increment msg in your code from Increment to IncrementCounter and reload. You would expect the count to still be 3, but it will actually show as 0 because, during playback, the old Msg silently fell through the switch statement in the update function.

The Elm debugger handles problems 1 and 2 by being privvy to the types used by the program that emitted the messages and refusing to load if the types have changed. It can afford to be very conservative in these cases because there is some expectation that if you were to load one of these history files, you would be loading into (more-or-less) the same app version. But in the case of hot module reloading, development is occurring at a rapid pace and things are changing frequently. So at a minimum, we would want a way to detect that the Msg playback may fail, and ask the user whether they would like to proceed anyway.

Problem (3) is fairly straight-forward: if you have an app that generates messages at high frequency (e.g. via an animation timer or mouse-move events), you can quickly collect 10s of thousands of messages. In my brief testing, this didn't appear to be a problem, but it was on my mind.

At the end of the day, I rolled-back the change and stuck with fluxxu's original implementation. It seemed more important to get it working with 0.19 than to change the way that it behaves.


Ultimately, I think the right thing to do is to switch to Msg tracking and build it on top of the Elm debugger. There are 2 things holding that back:

  1. Last I checked, the Elm debugger has performance problems which currently make it unsuitable to use as the foundation.
  2. API would need to be designed and implemented in the Elm debugger so that other tools could build on top of it.

@ghost
Copy link

ghost commented Dec 11, 2018

Hello ☀️

I'm experimenting here if you want to take a look. There's an example to quickly get started.

my local version of the repo is running a Mario-app I implemented to iterate on, as it updates on animation-frames. I have a lot of thoughts about using this kind of tool for a website/utility style application, but I hope to find a nicer way to support apps that update a lot more.

I'll add docs to the repo with my progress so it will be easy to follow along. Any kind of help would be much appreciated!

I also want to gathering information about how other languages/tools approach this and hopefully get inspired! I'll start with your gist, @rtfeldman

I plan to experiment with:

  1. Find a more suitable data-structure for storing large amounts of ordered and repeated (Msg, Model) values.
  2. Optimize the ZipList as it might be good enough with a proper implementation.
  3. Put holes in the ´(Msg,Model) values on the Model side, so that 10 updates stored would only store a single version of the model for each multiple of 10. When scrolling back and forth between states, I could have the data-structure go back to the nearest multiple of 10, and recompute the model from there. I think this could maybe trade space- for time-complexity...
  4. Drop the (Msg, Model) structure for something different. Do you know of any data-structures that are suitable for this use-case?
  5. I dunno ¯\(ツ)/¯ Maybe you have some ideas?

Thanks again for your guidance ⭐ @klazuka

@rtfeldman
Copy link
Author

So at a minimum, we would want a way to detect that the Msg playback may fail, and ask the user whether they would like to proceed anyway.

Totally agree with that!

What I mean by "jarring app state" in (1) and (2) is that when you reload the code, you expect that the visual state of the app remains the same with the exception of intentional changes that you made to how things are rendered.

I think that makes sense when tweaking visual elements, but I also think there are use cases where expectations differ!

I think in general we're on the same page with message replay being a good idea to at least try, but I came to a somewhat surprising conclusion when thinking it through, so I'd like to walk through how I got to that conclusion if you'll bear with me.

A direct use case for message replay

Let's say I'm working on a lengthy form, and I'm trying it out. I fill it out and hit submit, and I get a validation error. The validation error is a bug though; it was actually supposed to submit successfully.

So I go into update and fix the bug. Here are three things that could happen:

  1. (Message replay strategy) The validation error disappears. The fix worked! Hooray!
  2. (Storing model strategy) I still see the validation error because my Model's errors field still holds the same value as it did before I changed update. It's no longer possible for it to end up with that value, so if I refresh the page I won't be able to reproduce this state, but that's what my screen is showing me.
  3. (Force reload for non-view changes) I am forced to reload the page. I never see an impossible state, but I have to fill out the form again from scratch.

Tradeoffs

I can see some obvious arguments for (1) and (3).

The upside of (1) is that it saves me time. I don't have to fill out the form again. On the other hand, as @klazuka noted above, it can mean I end up seeing visually jarring changes.

The upside of (3) is that it reduces the incidence of impossible states being rendered, at the cost of developers spending more time retracing steps to get back into the desired state.

I don't see a clear argument for why (2) would be better than either (1) or (3). You don't get the time savings of the revised update logic being applied in-place, yet you can still have impossible states rendered. It seems like it combines the drawbacks of both (1) and (3).

The case for message replay

One of the reasons I lean towards (1) over (3) because "hot loading will never render impossible states" is not something anyone can rely on anyway.

Even if I only ever make changes to view, hot loading means it's possible to change event handlers in view such that no combination of user interactions could possibly result in this sequence of messages being sent to update, meaning if you refreshed the page you wouldn't be able to get back to where hot loading got you.

Since it's never safe for people to assume "if I see it on my screen, I'll be able to refresh the page and get back to it again" in general with hot loading, it seems better to make sure people are aware of this possibility than to nurture a false sense of security about it.

Naming

I think calling this feature something other than "hot loading" could help people understand what it does.

For example, I think it's very clear what a feature like this named "Automatic Message Replay" does: when you make a change, it automatically replays the messages.

I think if I told someone "automatic message replay is enabled out-the-box" as opposed to "hot reloading is enabled out-the-box" they would more fully understand what that meant, including the limitations, based on the name alone—literally no matter which of these design directions the "hot loading" approach went with.

Summary

  • Offering this feature in any form means potentially getting end users into impossible states they couldn't reproduce if they refreshed the page. Changing event handlers in view alone does that.
  • There are useful features, such as changing form validation logic in update, which require message replay.
  • Message downside has the downside of potentially getting end users into impossible states they couldn't reproduce if they refreshed the downside. However, we already have that downside no matter what.
  • Embracing message replay, and changing the name to reflect it, can unlock the upsides, while also mitigating the downsides we have no matter what—by making it more transparent to users what it's doing behind the scenes.

@klazuka
Copy link
Owner

klazuka commented Dec 13, 2018

Thank you for taking the time to write this up. Thinking through the edge cases is complicated—it definitely makes my head asplode at times.

I agree on all your points, namely:

  • storing the model has its own problems besides just difficulties in implementation
  • the term "hot reloading" carries a connotation from Erlang, Webpack, etc. which are at odds with message replay

I'm inclined to let @opvasger run with his effort to unify the debugger with live reload. The main downside of his pure Elm approach is that you have to write Json encoders for all your messages, which is not great for big apps.

Retrofitting this library to use message playback would require a couple of things:

  1. Change how things are named in order to avoid implying that it is doing state preservation on reload.
  2. Figure out how to get the message type information from the compiler so that we could safely do message playback.

One thought I had for (2) was to require that the app be compiled in --debug mode, in which case the compiler will emit the type metadata and pass it to the debugger's main entry point, which could be intercepted. But I ruled that out during my initial investigation because my main Elm app at work performs badly when --debug is enabled.

I am reluctant to take on any big, new projects. But if someone were inclined to make this work, I think the best thing would be to work on the Elm debugger itself so that it could vend type metadata to projects like this and @opvasger's elm-dev-tools. And make sure that there is not so much of a performance tax when compiling with --debug.

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