Elm Reactor grew out of my internship working on Elm at Prezi this summer. It improves the time traveling debugger created by Laszlo Pandy and Evan Czaplicki, turning it into a practical development tool. It has more features, a nice new UI written in Elm, and can now be used with any text editor. Elm Reactor is distributed with Elm Platform 0.13, so it is easy to install and use right now.
Check out the following video to see Elm Reactor in action when debugging a TodoMVC app written with elm-html:
Ultimate Undo Button
Elm Reactor lets you travel back in time. You can pause the execution of your program, rewind to any earlier point, and start running again. Watch me misplace a line piece and correct my mistake when playing elmtris:
In this example, I paused the game, went back, and continued to avoid crushing defeat. This is what “time traveling” means in Elm Reactor. It lets you:
- Pause a running program
- Step backwards and forwards in time
- Continue from any point in the program’s past
This sort of time traveling lets you explore the interaction space of your program faster. Imagine debugging an online checkout page. You want to verify that the error messages look right. There are several dozen ways to trigger an error message (e.g., bad phone number, no last name, etc.). Traditionally you would need to repeat the entire transaction for each error, slowly going crazy as you re-enter the same data for the 13th time. Elm Reactor lets you rewind to any point, making it easy to explore an alternate interaction. The next few sections will describe how Elm Reactor makes this possible.
Recording Inputs
All input sources to an Elm program are managed by the runtime and known statically at compile-time. You declare that your game will be expecting keypresses and mouse clicks. This makes the inputs easy to track. The first step in time traveling is to know your history, so Elm Reactor records these input events.
The next step is to pause time. The following diagram shows how an event such as a keypress or mouse click comes to the Elm runtime. When an event happens, it is shown on the “Real Time” graph and when your program receives the event, it is shown on the “Elm Time”. When Elm Reactor pauses Elm, the program stops receiving inputs from the real world until Elm is unpaused.
Events in Elm have a time associated with them. So that Elm does not get a hole in its perception of time, Elm Reactor offsets that recorded time by the time spent paused. The combination of event value and time means that these events can be replayed at any speed (read: really fast).
Safe Replay
Elm functions are pure, meaning they don’t write to files, mutate state, or have other side-effects. Since they don’t modify the world, functions are free to be replayed, without restriction.
Elm programs may have state, even though all functions are pure. The runtime stores this state, not your program. The input events dictate how the state will change when your program is running. Elm Reactor can jump to any state because this internal state is determined entirely by the recorded input events. The Elm runtime combines the previous state and new inputs to make the current state. So, to jump to any point in time and have a sensible state, you must replay the events leading up to that point.
The simple approach to time travel is to start from the beginning and replay everything up to the desired event. So if you wanted to step to the 1000th event, you would have to replay 1000 events. Elm Reactor uses snapshotting to avoid replaying so many events.
Snapshotting
Snapshotting is to save the state of your application in a way that can be restored. Elm’s version of FRP makes this straightforward and cheap. There is a clean separation of code and data: the application data is totally separate from the runtime. So to snapshot an Elm application we only have to save the application data and not implementation details like the state of the stack, heap, or current line number. This is most equivalent to saving the model in MVC.
Elm Reactor takes a snapshot every 100 events. This means jumping to any event from any other takes no more than 100 event replays. For example, to jump to event 199 from event 1000 Elm Reactor first restores the snapshot at event 100, then applies the next 99 recorded events. A better user experience strategy to snapshotting could ensure time travel never takes more than N milliseconds. This could be done by timing each round of computation and snapshotting every N milliseconds. Instead Elm Reactor uses the simpler snapshot-every-Nth strategy for its initial release.
Changing History
In addition to time travel, Elm Reactor lets you change history. Since Elm Reactor records the entire history of inputs to the program, we can simply replay these inputs on new code to see a bug fix or watch how things change.
In this example, Mario’s image URL and gravity were set incorrectly. Mario had already made a few jumps and time had passed. But the functions that control Mario could be swapped out because the functions are independent from their inputs. So despite having played with Mario, Elm Reactor can still swap in new code.
Playing a game while you build it is quite nice, but this is also remarkably handy for more typical applications. In the checkout example we described earlier, perhaps the last screen misplaced a close button. Once you navigate to that page, Elm Reactor lets you mess with the code as much as you want while you find the right place for the close button. You can see the results of your new code without maddeningly running through the entire interaction each time!
But what happens when you try to swap in a program with a type error or syntax error? In that case, Elm Reactor does not swap in the new code. Instead, it displays a message explaining the issue while the last working version keeps running. The following video shows this kind of feedback:
Try it yourself!
The editor below lets you try out all of the features described so far. If you click on the tab of the debugger panel it will slide away, showing more of Thwomp. Try it out!