Software development is fundentally about taking things apart.
I’ve been taught this concept along with a handful of other core
principles by Rich Hickey and the Clojure community over the last few years. I wanted to put it all together in a medium for my own learning and as an example to others (especially non-Clojurians) about how to build genuinely simple systems.
It’s my pleasure to unveil the Rush Hour platform. Rush Hour exposes the facilities to create highly accurate vehicle traffic simulations. It ships with a rules language to describe chains arbitrarily complex intersections with traffic lights and stop signs. Additionally, it visualizes the simulation using a dynamic heat map drawn on Google Maps.
It’s architected in a way that pays homage to the way I’ve been taught to build systems. I chose to build a traffic simlation because it’s a domain that’s highly familiar to most people, which minimizes the amount of learning you have to do to understand what it *is*, and maximizes the amount of learning you can do about its underlying principles. I’ll now
describe Rush Hour’s architecture.
The big idea
Rush Hour is composed of three major components: the simulation (Sim), Asphalt, and a web service called the Triangulation service. It’s quite simple.
The Sim is a loop that transitions from values to values. This transition is computed in parallel using Clojure Reducers. A storage abstraction of a few small protocols sits in front of an in-memory data structure that acts as a datastore for the Sim to use. The schema for city streets, rules about traffic, and timing of traffic lights are housed in this data store. The Sim exposes the current state outside the loop over an agent. One observer of the agent watches and serves up changes over a websocket streaming API.
Asphalt is a client of the Sim streaming API. It receives values emitted by the Sim. Asphalt analyzes the values one at a time and uses the Triangulation web service to determine a set of coordinates that describe the location of all the cars on the road. Asphalt itself exposes a streaming API too. A ClojureScript program listens to Asphalt’s streaming API and draws a dynamic heat map of the cars on Google Maps.
And that’s it. Here’s a live demo of a few blocks in Philadelphia. Caution: the rendering of the heat map is somewhat intense. It’s not mobile friendly.
Let’s zoom in on why this is interesting.
The value of values
As usual, a system based around immutable values is the way to go.
The Sim is process that’s about transforming one snapshot of the world into another. That’s all it does. It accomplishes this by applying a pure function to a snaphot to produce another snapshot. It’s a plain Jane infinite loop.
Since it’s all immutable, we can parallelize the computation with Clojure Reducers. That means the Sim makes good use of being hosted on beefier machines. All the logic to transition states is pure, and hence easy to reason about, easy to test, and so on. No concurrent semantics, locks, promises, deals with the devil, etc. Recreating state between components is a breeze since it’s just data - no objects, custom types, connections, or any of that yuckiness.
Just like Datomic and Simulant, we move time out of the equation.
There’s no notion of time in the code that computes the next state. Time passage is simulated at the tail end of the simulation loop with a single sleep. That value can be adjusted to control the rate at which the simulation runs as compared to ‘real time’. Each time the clock ‘ticks’, every entity in the system gets an opportunity to change state. One clock tick per second runs the simulation in ‘normal time’. Making the clock tick faster results in time moving faster within the Sim. Clocks with nonconstant properties can be used - I chose not to do this.
The Sim also pushes out another form of complexity. There is no notion of addressing or coordinates. We get much better reach by describing streets as lines of a certain length, and car position as a point on the line. This enables one to describe fictitious streets. No need to talk to Google Directions API in this component.
In order to be useful, we need a tiny amount of mutable state. The agent that holds the current snapshot takes care of this. There is a little more mutability though. I made the design choice that 3rd parties be able to “inject” traffic into the Sim at runtime. To accommodate this, each street has a j.u.c. blocking queue associated it. Anything sitting on the queue gets pulled into the street just before the end of the transition function. I consider this very controlled mutability, though. It’s uniformly operated on and has tightly isolated scope.
Data all the way down
"Data - please! We’re programmers! We’re supposed to write data processing programs. There’s all these programs, and they don’t have any data in them. They have all these constructs you put around it, globbed ontop of data. Data is actually really simple. There’s not a tremendous number of variations in the essential nature of data." - Rich Hickey
Data is king. Rush Hour has a rather small code base for how much it accomplishes. This can mostly be credited to the aggressive use of data.
Rules are data
Descriptions of laws of traffic for each intersection are maps.
They can be created in any language, by a human or program.
Rules use unification for a declarative style of conveying the laws of traffic. This is extremely powerful, as it obviates conditionals that would otherwise run rampant throughout the program.
Schema is data
The lanes themselves and how they connect are also maps. This makes them amenable to static analysis by a tool in any language. Additionally, integrating with Rhizome to create graphical pictures of roads is a cinch.
Duration is data
Time is pushed out of the equation by transitioning purely from values to values. But - traffic lights don’t update uniformly across the city at each clock tick. This fact can be conveyed as data to keep its meaning evident.
Navigation is data
Descriptions of individual intersections are isolated to facts about itself. To connect the streets of one intersection to another, it’s all data all the day.
Directions are data
We want to give a realistic depiction of how people drive around the city - not choosing streets at random. This gets represented as a weighted map to bias choices about where to go next.
From another angle, since it’s all data, we get terrific reuse out of all the constructs mentioned above.
Another more subtle point is the style of testing that this sort of thing allows. While I didn’t employ it, the door is wide open to generative testing because it’s all well-specified data.
Taking things apart
"I think one of the most interesting things about design is that people think its about generating this intricate plan - but designing is not that. Designing is fundementally about taking things apart. It’s about taking things apart in such a way that they can be put back together." - RH
This project was an exercise in taking things apart. I sincerely believe that this is the most import concept to understand as a designer.
Just at the edge of the Sim’s boundary is the mutable agent that contains the latest snapshot. It’s here that we can add a component that does one thing well. The streaming API watches for changes to the agent and pushes values to consumers. It’s involvement with the Sim ends there.
Similarly, other components can watch the agent without getting in the way. Implementing a HornetQ queueing communication protocol is an open operation. The same goes for other communication protocols -
they all get grouped together in one spot. Communication protocols are another thing you can take apart.
There are more things that we make simples out of. From the domain, we take apart streets, lanes, lights, light sequences, traffic rules, directions, and intersection connections. From the solution side, we take apart storage choices with protocols, clock implementations, navigation algorithms, communication mechanisms, and visual maps. All decomplected.
To the extent that just about everything is immutable, we can cache relentlessly. Rush Hour uses Elasticache and Clojure memoization (in-memory caching). This dramatically decreases network traffic, to the point where when all caches are hot, very little data crosses the wire at all.
"The other great thing about about conveyor belts and queues is that.. What do they do? What’s their job? They move stuff. What’s their other job? There’s no other job. That’s all they do." - RH
Rich has been saying this one for years. Put queues between the major components of your architecture. The architectural agility gained from having independence in the identity and availability of communicating parties is huge. Rush Hour’s simulation walls off consumers by exposing a single agent, and opening up its streaming and (future) HornetQ API off of that. This lets developers make use of the Sim from outside Clojure. Language boundaries are transcended.
Queues clear the way for an open, pluggable system. Adding durability of snapshots via Datomic and monitoring via CloudWatch or Riemann (or both!) are tasks that require no modification to existing
code. The power of queues lets us add more consumers (at runtime!) to react to change. It’s really great - Rush Hour has tons of connection points to build on, giving it a large surface area. If you want an open system, you do it like this.
Human vs. machine interfaces
"But of course, we also start to see the levels, right? If you look at the back of one of these modules, there’s another piece of design there. And these … are analogue circuits that determine what the module does. The other thing that’s interesting … is that each of these knobs has a corresponding jack. In other words - there’s a human interface, and a machine interface. … And the machine interfaces were there all the time. In fact, they were first. And then the human interfaces come. … You can always build a human interface ontop of a machine interface, but the other is often disgusting." - RH
The machine interfaces for Rush Hour are in place. Those multithousand line EDN files serve as a great machine interface, and not a bad human interface. I wrote them by hand. I didn’t particularly enjoy it, but it was easy to reason about. The pieces are in place for a human-interface to be built ontop of the machine interface.
"If you have designs and they specify things well, and you have some automated way to go from that specification to a test - that’s good testing. Everything else is backwards." - RH
"Test systems - not functions." - Timothy Baldridge
The Sim uses scenario based testing, and does very little
unit level work. It’s the same idea that core.async uses for testing. It has hundreds of lines of code that don’t have unit tests, and instead uses overarching tests to verify behavior. We don’t care to test implementation details. It’s a brittle thing to maintain tests for. If you can write overarching tests that make it easy too figure out what’s broken when tests fail, you are golden.
Coltrane couldn’t build a website in a day
"There are people who can make music by waving through hands through the air." - RH
I think one of the more interesting ancedotes about this project is that in its 5 month development effort, the entire first month was spent on the hammock. I spent a lot of time in June laying in Rittenhouse park working out the complexity of the problem space. Coding didn’t proceed until I was finished taking things apart, and ready with a solution to put the pieces back together.
I was sort of forced into this period of hammock time. I moved to Philadelphia at the beginning of June and didn’t have internet for a few weeks. With my evenings completely free of distractions after work, I had time to diassemble the problem space. The hammock period was by far the most difficult phase, and bordered on excruciating at some points. It’s hugely frustrating to remain patient and work past the feeling of producing no code. That is, of course, merely the voice of insecurity tempting you to act early. Take the time to hammock. It sets the stage for an incredible show.
For a few months, I was a man possessed building this system. It’s not perfect (or perhaps even useful!), but I hope it serves an example for you to learn and teach others.
A huge thank you to a few people that helped me create this. Timothy Baldridge gave me plenty of guidance in building the Sim. James Drogalis did the math for me to compute coordinates based off of simulation data.
Tweet at @MichaelDrogalis or email at mjd3089 at rit dot edu, or mdrogalis on #clojure IRC. There’s not much in the way of documentation, because I honestly don’t expect anyone to use this project so much as use it as a guide for learning. If you want to build on it, I’d be happy to hook you up with some docs.