Elm FRP in Clojure core.async
Summary: Elm is an exciting FRP language. I implemented the FRP part in Clojure using core.async.
I like to read research papers. I have ever since I was in high school. I've always wanted it to be pretty easy to just translate the pseudocode for an algorithm for a paper and then have it working without any trouble.
Of course, this rarely happens. Either the pseudo-code leaves out important details, or it's expressed in terms that are not available abstractions in any language I know. So then I am left to puzzle it all out, and who has the time?
A few months ago I read the fantastic thesis by Evan Czaplicki^1 where he introduces Elm. It includes a novel way to express asynchronous Functional Reactive Programming (which he calls Concurrent FRP) and a way to express GUI layouts functionally.
I highly recommend the thesis. It is very clear and readable, and points you to valuable resources.
The reason I was reading it was because I think FRP has a bright future. It's definitely got potential for simplifying the way we write interactive applications. I wanted to learn how it works so that I could build bigger castles in the sky.
Elm FRP is pretty cool. It is, first of all, very few parts. You build bigger abstractions from those small parts. It is built so that events trickle through the graph in order. In other words, they're synchronized.
But the other interesting thing is that sometimes you don't want things to be synchronized. What if one of the calculations takes a long time? You don't want the GUI to stop responding to mouse events while that slow thing is happening. Elm lets you segment the graph into different subgraphs that can propagate concurrently.
Seriously, just read the thesis.
While I was reading it, I got to this page where he lays out all of the FRP functions in Concurrent ML. It's not uncommon for entire Computer Science theses to be written, passed, and published without a line of runnable code. But here it was, in real code (no pseudocode). I don't know ML, but I do know Haskell, which is related. And I started puzzling through it, trying to understand it.
Elm FRP in Concurrent ML
By flipping between the text and the code, I got it. And then I realized: everything I needed to write this was in Clojure with core.async. So I got to work.
It took a while of playing with different representations, but I got something I can use and that is pretty much a direct translation of the Concurrent ML code from the thesis. I toyed with some deeper factorizations, but I think they only muddy what's going on. And it runs in both Clojure and ClojureScript. It's available on Github.
And, of course, I needed to test my creation, so I ported the Mario example from the Elm website. You have to click on it to start it. Use the arrow keys to walk left/right and up to jump. It captures your keystrokes. Click outside of the box to get your scrolling keys back.
Now, I didn't write all of the cool GUI stuff from the second part of the thesis. I was learning Om so I decided to use that. That's why that part is so long. Writing Om components is basically as long as writing HTML + CSS.
And Elm is a language built around these FRP abstractions, so a lot of the signals are built in. I had to write event handlers to capture the mouse clicks and keyboard events. But right in the middle of mario.cljs, you'll see a very straightforward translation of the Mario example from Elm.
There are a few differences between the Elm version and the Clojure version. Clojure is untyped, so I was able to eliminate a channel used only to carry an input node id. That instead is passed along with the message on a single channel.
Also, I added a node type called "pulse" which I use for emitting time deltas for calculating physics. I'm not sure if Elm internally does something similar for its timers but it seems like the correct solution. The trick is you want to emit a zero time delta while other events are firing, and an actual time delta when the timer ticks.
Finally, instead of the graph being global as in the thesis, it's all tied to an "event stream", which is where you send the events which get channeled to the correct input signal. You can have multiple event streams and hence multiple graphs.
The implementation is very straightforward. You have to know the basics
of Clojure core.async, plus mult
and
tap
.
core.async brings Clojure a new set of abstractions that let us tap
into more research and learn from more languages. And that makes me
happy :)
If you'd like to learn core.async, I have to recommend my own video course LispCast Clojure core.async. It teaches the most important concepts from core.async using story, animation, and exercises. It is no exaggeration to say that it was the most anticipated Clojure video when it came out. Go check out a preview.
- Go say Hi to him!