Tuesday, 2 September 2014

Mars Rovers in Clojure - Rewriting an Akka project to a Core Async one

I've decided to to reimplement Kaloz's Mars Rover Scala (for brevity's sake SMR from now on) project in Clojure (CMR). SMR is based on Actors of the Akka framework. Actors are the manifestation of the original idea of object orientation, that is, independent entities hiding their internal state and communicating via message passing. Since the whole model is purely about side effects, it seemed a nice challenge to implement in a functional way.
I wanted to have a jar as an end product that can be substituted to the backend of the original Scala, using the original presentation layer. I'm not there yet, but a first version is available on Github, where mars rovers can run in the REPL, spitting out their positions simply to the console.
All in all the project is a nice round 600 LOC. The Scala version had 548, which is not surprising. Scala, much like Clojure, is a very concise language, but because of its syntax you can compress the code more and still retain readability.

The main differences between SMR and CMR are
  • Instead of complecting state, identity and behavioural logic, the Clojure version uses pure functions to capture the behaviour.
  • All the "actors" have a receive method that takes the state of the "actor" and the received message (plus some additional information sometimes) and returns the new state with possibly a list of messages and executable effects. 
  • Context changes in actors are implemented in a surprisingly elegant way via multimethods. Different version of receive is called based on the state of the "actor"
  • The actors are simple atoms. The state of the atom is the input of the receive and the state part of the output is swap!-ed into it
  • The Clojure version, for the time being, lacks the distributed nature of Akka, and also its error handling mechanism. 
  • It uses the core.async for asynchronous communication. Where in the Scala version actors hold references to other actors, here channels are passed around
  • The architecture, not surprisingly, is quite different

Application = 75% pure functions + 6% glue + 8% application-level + 11% other

During the reimplementation I dissected the code to form 4 distinct parts. LOC-wise the sizes of these parts are
  1. 75% is pure, framework-neutral functional code. I moved all those source files under marsrover.pure package. 
  2. 6% is the glue, a mini messaging-framework I built over core.async in marsrover.glue.clj. In this "framework" a component's state is preserved in an atom. The component has an in-channel, an atom, and a function defining its behaviour. When a message arrives on the channel the component's state (value of the atom) and the message are the inputs of the behaviour function, and the output is the new state, the messages it has to send out, and possibly some effects. It's all very high level, doesn't know anything about our particular domain. 
  3. 8% is the application layer, it's in marsrover.app.clj. This takes the pure part and injects it in the glue
  4. 11% is other stuff like reading the expedition config (number of rovers, size of the plateau, etc).

So there is a big bulk of pure functional blob and a very thin layer glueing it together to form a proper application. I find this really fascinating. In the original Scala/Akka implementation the Akka framework and the logic was undisentangibly intertwined. Now the glue uses core.async, but if I wanted to use an actor-framework instead or any other approach, I would only need to change that 35 lines confined in a single source file. I say it again, it's fascinating. With functional approach we can push the framework/technology -related code to the outmost layer of our application, making it seem almost insignificant. The pure functions won't change if we want to move from the asynchronous model to synchronous, or replace core.async with any other integration solution. Imagine you'd wanna rewrite Mars Rovers in Scala, but not using Actors.

Finally, you can run the app by running marsrover.mars_expedition.clj in the REPL.


2 comments :

  1. Nice remake but I still prefer my scala version!!!
    ;)

    ReplyDelete