Elm devTools - Interactive devTools for Elm!

Hi :slight_smile: I’d like to share an experiment I’ve been working on recently:

Preview

This is the example that comes with cloning the Github repo:

I love using Chrome DevTools, so I decided to have the debugger fit its visual look/feel .

Goals

My goal was initially to improve debugging in Elm for myself. Based on my own experiences, I wanted a couple of things:

  1. reliable hot-reloading, for the debugger and the app it wraps.
  2. a slider to scroll back and forth between app-states (time-travel)
  3. an infinite list of updates performed, synchronized with the slider
  4. a simple way to import/export the full app-state from/to files. (bug-reports)
  5. an option to overlay an app with it’s model at any given time (model overlay)
  6. a button to unsubscribe/resubscribe the app with a click
  7. have the debugger collapse/expand, drag/drop, or dismiss easily.

I’m requesting feedback because I’ve managed to implement these features :sunny:

  • I hope some of you will try out the example and give me some feedback on what you think about its current state and the direction you’d like it to go, should you want to use it.

  • I plan to do my own review of other solutions to similar problems. If you know of any really nice tools in other languages/ecosystems that haven’t already been mentioned, let me know! :blush:

  • Suggestions on a suitable data-structure for the absurdly long sequence of Msg would be much appreciated. I’m don’t have a deep theoretical background that allows me to make good decisions in this problem-space. The current approach relies on a naive ZipList (Msg, Model) backed by List. I have a lot o ideas, but could use some insights or inspiration :sunny:

@klazuka has been really enouraging, sharing insights about elm-hot and HMR, as it is a quite different approach to some of the same problems I’m trying to address here.

Caveats

There’s a couple of problems that I haven’t gotten around to solving (or can’t solve at all):

  1. This is my initial implementation which is backed by a naive ZipList for navigating states. It doesn’t perform very impressively and starts to feel sluggish on my machine at ~1000 or more updates. There’s a lot to do in this area, but I want to make sure that the design is good before optimizing.

  2. “Breaking changes” to the Msg-type in your program should be the only way to force your app back to the initial state.
    “Breaking changes” in this context means changes to Msg that doesn’t make the new Msg-type a superset of the old one. What you generally want to do is to make append-only changes to your Msg-type if you want to rely on hot-reloading and time-travel. Beyond Msg, you can make modifications across your app and shouldn’t experience any issues.
    An improvement in this area would be to decode stored Msg up until the point of the first breaking change, and only return Msg performed before then - I haven’t gotten around to do this yet, but it should be trivial at this point.

  3. As you can see in the example above, you can achieve some very dynamic behaviour from making changes to your update-function after running through a lot of states. I’m uncertain if this is only an upside or could cause real problems in practice.

  4. Hot reloading relies on a flag from JavaScript and a port to JavaScript. you can see it here and here. I’ve done some iteration to trim it down as much as I can, and I don’t think there’s any solution to this beyond an Elm-native LocalStorage API or something similar. I experimented with using HTTP or Websockets, but I decided that I’d rather have the module on the Elm package-manager than rely on a server. The server approach did open up for live-reporting compiler-errors in the browser, but they are already well-supported in IDE/Text-Editors + it would add an extra dimension to the package. From this point, I focused on making the module compatible with the Elm package-manager (which it still is!).

  5. If you scroll back to the initial state and toggle subscriptions on, the program will bypass the commands from your init-function. This can be mitigated by reloading the browser from the initial state, but I haven’t found a way to solve this properly yet. I’m sure it’s possible though.

  6. A Json.Decode.Decoder Msg and Msg -> Json.Encode.Value has to exist for my pure Elm solution. There is a possibility that this won’t be necessary in the future, but who knows what might happen. One might argue that it is unacceptable to have to write two such functions, but in my experience, this isn’t a big problem in practice. Sure, retrofitting the debugger onto your app require you to write up both functions from scratch, but it’s fairly easy to incrementally do this while developing your app. I think it’s pretty good practice to keep such functions near your Msg type, which is where the changes will cascade from in the first place.

Looking forward

I hope to see this kind of solution prove efficient enough for almost any Elm app. I’m not sure if this will exist as an Elm module with it’s current constraints, or if there will be APIs to get around some of the current caveats before then.

I hope some of these ideas and experiments can help drive the debugging/development experience in Elm forward. From the initial debugger presented by Evan a couple years ago, to the release of the Elm 0.18 debugger, I hope to drive this vision forward and maybe help you be more productive in your work :sunny:

Let me know what you think!

Asger

48 Likes

I really like this approach of unifying the time-traveling debugger with hot reload. They are very closely related, and I think it makes a lot of sense to see how they could fit together into a cohesive whole.

Furthermore, the existing approach to do hot code reloading used by klazuka/elm-hot (and fluxxu/elm-hot-loader before that) is complicated, brittle and evil. They inject code into the Elm runtime and hook the core platform functions. It’s difficult to maintain and highly invasive. I hope that at some point Elm itself can vend an API that it makes it easier to do this (perhaps by giving the debugger API to export & import its history). And it seems like this work by @opvasger is a great step towards figuring out what that might look like.

:+1:

9 Likes

This looks amazing, thanks a lot for taking the initiative for trying to improve the status quo!
I will definitely have to try this out on my projects.
I’m especially interested in how well it can handle tons of messages, e.g. from a subscription to animationFrame

Since you haven’t mentioned what you already know, I’ll start :smile:

Did you know about jinjor/elm-time-travel? It hasn’t been updated to 0.19, but it’s pretty cool. I especially like the model diff view it has.

You almost certainly know about the redux devtools, but because you didn’t mention it, I thought I let you know, just in case.

Also, if you don’t know Bret Victor, you need to at least watch this.
If you haven’t seen this already, you’ll see where Evan took his inspiration for the first version of the debugger…

Hope you find something new there.

Btw, can you debug your own debugger? :thinking:

1 Like

It looks really nice! And it is nice to see the Mario example again! :blush:

About tricks to display many messages, the trick I use in the existing debugger is to make sure that lazy is used in increasingly large blocks. So instead of being a linear list of items, it is more like this:

...............................................................
\      /\      /\      /\      /\      /\      /\      /
 \    /  \    /  \    /  \    /  \    /  \    /  \    / 
  lazy    lazy    lazy    lazy    lazy    lazy    lazy  
     \    /          \    /          \    /     
      lazy            lazy            lazy
          \          /
           \        /
              lazy

I think the particular way I did it is that there are only two outer items: there is (1) a node with the latest messages (between 0 and 128 messages) and (2) a node with all the “older” messages. When the newer messages pile up to 128, I put them in a block together and move them to the older messages. Those blocks are then arranged into a tree of lazy pairs in the old section. Note: the data structure needs to mirror all these details so the reference check in lazy actually works!

This gives the two nice benefits. First, when adding new messages you are always looking at <128 nodes in the diff, which is technically O(1) because it is bounded. Second, when you are going back in time and changing colors of past messages, you only need to do a diff the relevant parts of that tree of lazy nodes. So if it is in the first block, you only disrupt three lazy nodes and do a full diff on only the relevant block. This is O(log n) rather than O(n).

I also only hold one Model per block of 128 messages to trade space for performance when going back in time. I never profiled to see what the right tradeoff is for applications that produce tons and tons of messages. Maybe some other block size is better. Maybe the block size is good, but having more “cache points” is a better tradeoff. Maybe there are other tricks. No strong intuition on this!

Let me know if you have any questions about this, and feel free to contact me on Slack about particulars! I am separately curious to know if there are ways to make the debugger less dependent on me (as I feel like a maintainer rather than author with it) and you may know well if there is some small API I can provide instead of the full UI!

Anyway, again, it looks really nice! Hopefully this info is somewhat helpful!

13 Likes

:rainbow:

I’ve seen the video multiple times, along with his other videos and related work on “dynamicland”. It’s really inspiring to think about his principle of “immediate connection”. I think his vision for immersion is exactly what “hot-reloading” and “time-travel” is all about.

I wonder if more if his presented ideas fit into this set of features? Do you think we can get to a point where we can make the leaves on the tree dance, like in the video? :sunny: I will definitely rewatch the video! Thank you.

I will open an issue on Github to track the review of prior arts. Thanks for your suggestions! :sunny:

Haha! my current API is a mirror of the Browser programs, so I can’t nest them unless I fragment the API a bit. I think something like that would make for a cool demo!

:rainbow:

Thanks a lot for your guidance - I’m excited to see what kind of results I can get using this data-structure. I’m pretty confident your description is thorough enough on it’s own, but I’ll let you know if anything comes up :sunny: I guess I’ll start to measure once I get to a point where I can adjust block-size and cache-points!

I think keeping the project publishable on the package-manager is the way to go! It will take some time for me to find a good suggestion for what this small API should look like. I’ll contact you about it once I get there.

I believe Lazlo Pandy developed the first iteration of the Elm debugger? Anyways, I hope this can turn into a proper 3rd iteration of that vision, and perhaps let yourself and @klazuka focus on other goals!

Thanks again - I look forward to sharing some results! :sunny:

Asger

5 Likes

Yep! Here’s a real throwback of a video: https://www.youtube.com/watch?v=lK0vph1zR8s

1 Like

Yes, the whole idea came from Lazlo Pandy who presented the ideas and initial implementation here in 2013.

I was personally frustrated by Bret Victor’s demos. As I recall, the programming language shown in those videos had mutable variables and side effects, both of which make backtracking practically impossible. People had already tried it even, like in Efficient Debugging with Slicing and Backtracking about gdb in 1990. Tracking what happens in memory, with system calls, file state, etc. is very costly. (E.g. how do you preserve that two pointers point to the same address without copying all of memory?) I’m not very knowledgable about this area of research, but as I understand, the amount of memory needed to save reliable “slices” of the program made it infeasible in practice in many cases.

In contrast, Laszlo was inspired by Bret Victor’s demos. He realized that Elm happened to make language design choices that sidestep many of the crucial practical difficulties. So I think he deserves more credit for finding a more practical approach for backtracking. This approach has since been explored a bit more in Elm, and subsequently in JS as well.

Note: Bret Victor’s demos happened when crowdfunding was more popular. A couple people at the time ended up raising quite extraordinary sums of money based on cool demo videos that clearly needed significant theoretical breakthroughs to be plausible. I think many folks assumed that those videos were real (reasonably!) and did not have the background to see the theoretical difficulties.

10 Likes

Lazlo was really ahead of his time-- there’s no doubt that some of the problems of Light Table and Eve were due to mutability, and the subsequent difficulty of managing a visual syntax and a textual syntax when editing. It’s interesting that many of Victor’s ideas are resurfacing in the Luna languagehttps://www.luna-lang.org/index.html. Like Elm, the compiler is implemented in Haskell, and Luna is a purely functional, lazy language. IIRC, their current stack uses React for the front-end. The github repo is here: https://github.com/luna/luna

https://news.ycombinator.com/item?id=16163769
https://news.ycombinator.com/item?id=14612680

1 Like

I actually downloaded the beta some months ago and tried it out!

I remember watching a video of Linus Torvalds presenting git at google. During the talk, he describes how cheap branches changes the way you work in a quite fundamental way - I think the same is true for closing the loop between writing code in a text editor and interacting with it in the browser! This is the idea that got me working on this project in the first place. Bret Victors presentation echo the same ideas :slight_smile:

3 Likes

Cool. Really excited by your ideas, and innovating with dev tools!

1 Like

Thanks for working on this. An improved debugger would be of great benefit. If you haven’t seen the Meiosis dev tracer, it may offer some good inspiration:

Meiosis is a pattern (no libraries or framework) for handling state in JavaScript that is much simpler than Redux. I especially like the dev tracer’s input feature that lets you change your model manually. It would be very handy to change a Model or fire Msg's at will in an Elm app.

4 Likes

Thanks - I will review the tracer along with some other projects here :sunny: Meiosis and its tracer looks really cool!

In an early version of my debugger, I had a “commands” tab, where you could fire sequences of messages using labeled buttons. These buttons (and their sequences of messages) could be specified through a configuration. I was catering to a use-case along the lines of:

My app is growing big, and I’d like to have shortcuts to specific app-states where I can continue development from.

I decided to remove this functionality because:

  1. If you save a history of app-states to a file, you can just use that file for shortcuts to app-states.
  2. Firing messages from outside your view, subscriptions and init functions can lead to states that wouldn’t otherwise be reachable - incentivizing you to deal with isssues that wouldn’t be able to come up in production.
  3. It required configuration to setup these buttons to fire messages. You’d also have to change the config every time you changed your Msg-type. Nobody likes configuration :sunny:

If I’ve missed something, any input or guidance would be much appreciated. I want the best possible result, and compromising my own ideas to get there is :100: ok.

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.