Rewriting the ThoughtWorks Tech Radar in Elm

Written by ckoster22 | Published 2017/05/25
Tech Story Tags: elm | functional-programming

TLDRvia the TL;DR App

A while ago ThoughtWorks released an open source repository for anyone to host their own personal technology radar.

I was curious to see how the reputable company implemented it and strangely I noticed a lot of concepts that reminded me of Java, such as stateful objects and error handling with try/catch/exception.

All the application really does is take a single input and produce a tech radar. A Java-esque approach seemed inappropriate for that use case so I decided to rewrite some of it in Elm!

Step 1: Model the radar

Let’s start things off by thinking about how to model the radar itself. If we model our problem carefully it can simplify the work ahead of us.

First let’s consider what we know about the data. The radar is split into four quadrants and four rings. These quadrants and rings represent different categories that several blips can be placed into.

Since we know there are only four quadrants and rings let’s represent those as union types.

So far so good. Next up, the blips.

Modeling the blips requires some extra thinking because there are two representations of a blip.

The first representation is the blip data from the Google Sheet (example). The information included is the name, ring, quadrant, an isNew flag, and a description.

The blips that are visualized on the radar need those same 5 properties but in order to be displayed on the radar they also need an x-y position.

Several ways to model these blips include:

  • Throw all properties into a single record and make the type of the position field a Maybe. This option uses fewer types but will inevitably lead to more maybe handling.
  • Treat the blips from the Google Sheet as a distinct type and record from the blips visualized on the radar. This option removes the maybe verbosity but at the cost of duplicating data in two separate record fields.
  • Since the properties of the radar blip are a superset of the blips from the Google Sheet we can represent the data with composition. A blip positioned on the radar will have the Google Sheet blip data in addition to a defined position.

There are other options worth considering, especially options that leverage union types, but the third option will suffice for now and we can refactor later if needed.

Next up is visualizing the radar.

If you’re curious on how we’re doing in comparison to ThoughtWorks’ open source example then swing on over to their OO equivalent modeling.

Step 2: Visualize the radar

Now that we have the basic building blocks to construct a view let’s start building the radar visualization.

The first thing I want to do is get the quadrants and rings visualized. We’ll heavily use SVG and borrow a lot of the styles from the ThoughtWorks open source project. I’m sure they won’t mind.

Creating an arc

The radar isn’t a circle. It’s technically four quarter-circles put together, a.k.a arcs.

Circles in SVG are not that hard. Arcs, on the other hand, suck big time. Here’s a demonstration of what I mean.

With the above code working it will be exiled to a utility module so that we don’t have to think about the implementation details for creating an arc in SVG.

Visualizing quadrants

Visualizing a quadrant only requires that we visualize 4 arcs (rings), each with a different radius.

Not bad. If we do this three more times and provide different numbers we get the entire radar background.

Visualizing blips

There are two types of blips that can be visualized which is determined by the isNew flag. One is a rounded triangle and the other resembles a smudged circle.

I’m going to simply transcribe ThoughtWorks’ code for these blip visualizations to Elm.

..more SVG implementation details that we’re not going to look at ever again.

Next up, visualizing mocked blips on the radar.

Step 3: Wire up the view with mock data

Before we consume actual data from a Google Sheet we’re going to mock up some data to make sure our blips are correctly displayed on the radar.

The above function is temporary but it will produce 16 blips on the radar, one in each ring in each quadrant. However, this brings up the question of where exactly to position the blips.

I went ahead and peeked at how ThoughtWorks did it. In short, they produce a sinusoidal curve that a blip is to be positioned on and if it collides with another blip it will try another random position on this curve.

We can do the same thing in Elm but it brings up an interesting topic.. how to do randomness in a pure functional language.

<quick detour>

Pure randomness with generators

In Elm, every function is a pure function. In order to produce randomness we can ask Elm to generate us a random value for a given seed. A random value and another seed is returned so that we can use that seed to ask for yet more random values.

However, if we’re producing many random values then managing these seeds becomes extremely tedious and prone to mistakes.

Fortunately, Elm gives us the concept of a Generator. According to its documentation, a generator is like a recipe for generating certain random values.

Here’s an example generator that can be used to produce random integers from 3 to 7.

Random.int 3 7

Here’s another generator for producing a random list of even values under 100.

Simply creating a generator doesn’t actually produce a random value. It still has to be passed to Random.step .

(randomEvens, nextSeed) =Random.step (generateEvenValues 10) (Random.initialSeed 123)

Given this knowledge we can compose a few generators to randomly position blips along a sinusoidal curve and keep producing values if there is a collision. This will behave identically to the ThoughtWorks algorithm and it also happens to be where we convert the List GoogleSheetBlip to List PositionedBlip .

Randomly positioned blips with Generators

I realize we went from 0 to 60 with generators in about 90 seconds. If the above code is not immediately understandable, don’t worry. Like much with Elm, it is initially unfamiliar but it very quickly becomes second nature to read that code.

</quick-detour>

Blips visualized with randomness

When we run the mock data at the beginning of this section into our view function we get a radar with blips on it.

At this point we can keep throwing data at the radar and verify that the algorithm for determining blip positions is working.

The positioning here seems reasonable. Moving on.

If you’re tracking progress here’s how ThoughtWorks is doing the same. It’s also worth noting that the radar is mirrored along the y-axis because we hard coded the random seed. That’s fine.

Step 4: Model the app state

Just as before, modeling the app state is incredibly important. When writing your own Elm applications it’s worth pausing, discussing, and considering the trade-offs for how to model your data.

For this application we already have a radar but we’re missing a page to enter a Google Sheet url.

I chose to model that page state like this:

It will have a form with an initially empty url. The form might have an error and we’ll want to know if we’re actively retrieving the Google Sheet. There are alternatives to this approach, but we’ll stick with these three properties for now.

That covers what I’m calling the Landing Page. Lastly, there’s the Model that needs to be provided to The Elm Architecture.

This is loosely based on Richard Feldman’s SPA example except with a few things cut out for the sake of simplicity, such as not dealing with routes for such a small application.

It’s important to point out that at a high level this application is in one of two states:

  1. We’re on the landing page and interacting with the url input form
  2. We’re on the radar page interacting with a tech radar

Step 5: User input, regex, data retrieval, and error handling

The landing page involves receiving input from the user, parsing out a Google Sheet ID, performing an HTTP request, parsing the response, and handling errors in all of those cases. I’m going to skip the user input part because if you made it this far you know how to do that already.

If I’m wrong and handling user input in Elm is new to you I suggest pausing and checking out this example.

Regular expressions in Elm

We’re about to go 0 to 60 again. Hang on.

The user will supply us with a link to their published Google Sheet, such as “https://docs.google.com/spreadsheets/d/11Fd0lwNIEUs2ymxNEiTfpM5CoQHQbMuOId8TjrQHDn0/edit?usp=sharing”.

Hidden within this URL is an ID that we need to perform an HTTP request. It’s that unreadable string between “/d/” and “/edit?”.

First we’ll declare a regular expression that will find that ID. It goes without saying that credit for this regex goes to ThoughtWorks again.

Within that regex are groups and we care about the first group specifically.

In order to apply a regex to a string we use the find function.

matches =find All sheetIdRegex url

That returns a list of matches that take the following shape.

Our Google Sheet ID is hidden within the submatches list. So what we want to do is get the first match in the list of matches returned from find , and on that first match find the first item in its submatches .

If you’ve worked with Elm lists and List.head before you’re probably already aware that we’re about to go head first into MaybeLand. Thanks to elm-community/maybe-extra the code to pluck off that ID and produce a Result isn’t too bad.

The first MaybeExtra.join collapses the double Maybe that occurs when you do a List.head on a List of Maybes. However, it’s still a Maybe returned from Maybe.map so the second MaybeExtra.join collapses it one more time before converting it to a Result.

Retrieving the Google Sheet data

Once we have the ID we can make a request to retrieve the Google Sheet data in CSV format.

When this Cmd is performed it’ll call httpResultToMsg with the Result.

The above function does the following

  1. On success, split the string by new lines
  2. Iterate over each row and attempt to parse the CSV as a blip
  3. Successfully parsing data puts it in the blips list while parse errors are added to the errors list.
  4. Send off a Msg with the blips and any errors

Parsing each CSV row is as simple as attempting to map the data onto our data model defined earlier.

Conclusion

This post is getting long so I’ll wrap things up by making a few observations in rewriting some of the ThoughtWorks Tech Radar.

The BYO radar project used chance for randomness, d3 for graphics and HTML templating, and tabletop for parsing Google Sheets. It uses stateful object instances to model its data and incorporated exception handling rather than gracefully handling errors.

Most of all, after spending so much time in that code I still don’t understand what much of the code does. The code is fairly complicated and even with understanding the desired behavior it’s difficult to track the implementation details.

On the other hand, a functional approach with Elm yielded more comprehensible code with similar functionality. In fact, once you grasp the way Elm does randomness and regular expressions you’ll realize the code is much more simple too.

To see the partial rewrite in action check out these two links:

http://ckoster22.github.io/elmradar/

https://github.com/ckoster22/techRadar/tree/blogpost


Published by HackerNoon on 2017/05/25