Here’s a tutorial on using ghcid to live reload a dear-imgui application. This post also introduces how to use the upcoming cabal 3.12 multi-repl feature to reload the executable while working on the library:
Very cool!
I believe the killer feature is the REPL interpreted mode which lets you run the code without linking, which is often the slowest part of a live reload workflow. For example, even with all the tweaks prescribed in the Bevy Rust engine compile optimizations doc, including using the new mold linker, I couldn’t make my toy moonracer game reload in less than a few seconds.
I 100% agree. ghci
is a top reason why I continue to use Haskell for gamedev and consider languages such as Rust to be insufficient.
Speaking from gamedev experience, you can probably take the hot reloading even further. For instance:
- You can re-use the existing window handle by storing it with
foreign-store
. - Dunno if imgui could do this, but you can spin up your app in an OS thread and have it listen on an
MVar
for an entire app loop. Stuff thatMVar
in aforeign-store
and then on reload, pass in the newly-loaded app loop. Depending on how your state & loop is managed, you can have new code operate on pre-existing state. No need to bouncemain
! I’ve done this with games andapecs
and it’s pretty cool. - You can also use the
MVar
approach to makeghci
your development console - have your app loop listen to anMVar
that has effects in it and execute them. You can even have it pass results back with someDynamic
magic - [example code]. I banged my head against the wall trying to add a “dev console” using a hand-rolled interpreter for a while. And then I triedhint
. But then it dawned on me thatghci
was all you need.
Thank you @Ambrose for such great feedback! I initially meant to write a response to the loglog post about leaving rust gamedev by explaining how their arguments do not match the user experience I get with Haskell. Though this quickly got unwieldy and I decided to focus on showing the feedback loop instead. I am glad to hear I’m not the only one using Haskell for that particular capability
Now am I thinking about writing a few more parts such as using an IDE with HLS (import completion, go to definition, …), hot reload as you suggested with foreign-store
or essence-of-live-coding and perhaps finish with binary distribution.
Though I’m also trying to keep this guide as simple as possible so that new comers can give it a try without much complications.
Thank you for your work on macaroni and the mayhem-engine, I’m looking forward your next release!
I wonder if any of these live reload strategies eg foreignstore could work in a Servant web backend?
My dev workflow would greatly benefit from being able to make lucid html changes and instantly refreshing the browser to see the changes.
For WebUI, you can get live reload by opening a websocket and calling window.location.reload()
in the javascript reconnection handler, that way, when your backend restart, the web client can automatically refresh the page. Here is an example with htmx: demo-websockets, and L140 for the setup.
A note from my experiments here:
Some context window creation library like GLFW
require the program to be run from the main thread, but GHCi can launch your program in a fresh unbounded thread. In any case, the workaround is
:set -fno-ghci-sandbox
before running Main, to force computations to be run in the main thread rather than in a forked thread.
I should note that for 10 years foreign-store has remained the same with no commits, with a terrible segfaulty bug that nobody noticed was there, until a few months ago when Johannes Gerer fixed it: GHC segfaults (aka "The perils of using a non memory safe language") · Issue #4 · esoeylemez/foreign-store · GitHub
So if you tried foreign-store in the past and experienced segfaults, it might be fixed now.
I just gave foreign-store
a try and it worked really well for this use case, thanks you @chrisdone! I asked a question to improve the ergonomics with ghcid: How to check if a global store is initialized · Issue #6 · esoeylemez/foreign-store · GitHub .
It looks like a great workflow for persisting the window across reload (and avoid loosing the focus on the IDE), and I’m now tempted to propose simple wrappers for sdl, glfw and gloss to handle the hot reload.
I should note that I’m also using pulse-simple to submit audio buffers near real-time (30 times per second), the library was last uploaded 13 years ago, and it still works fine with ghc-9.6. Makes me wonder what is the oldest package still working today As far as I can tell, Haskell is remarkably stable!
IIUC, when performing a reload in ghci the REPL bindings are lost, but any already started thread will keep chugging along, still running the old code?
I wonder if we could harness that to create an alternative to foreign-store which, instead of FFI, used helper threads to store values that could later be queried using asynchronous exceptions. I tried that in this repo; it’s very hacky but it kinda seems to work.
Perhaps some form of binding persistence functionality should be integrated in the :reload
command itself. Seems like a useful feature, despite the dangers.
Does this mean all the threads of your program need to be aware of receiving this exception? Oh, nope, you’re using labelThread to identify the participating threads, even if it is O(n). Both listThreads and labelThread are opening up interesting possibilities!
Another use case that springs to mind would be a chaos monkey style thread killer, which would highlight how robust your thread supervisors are.