Invalid State in Elm — Stay Relentless

Constant Refactoring is a Good Thing

billperegoy
im-becoming-functional
7 min readNov 7, 2017

--

There’s No Ideal Linear Development Process

Over the past few weeks, I’ve been sharing my lessons learned in developing a dart scoring application in Elm. It’s by no means a big application, but I’ve been attempting to be very formal in the way I develop it. Last week, I defined the general process I’m using. At first glance, it seems very linear.

  1. Spend time iterating on the types. Play with them, compile the code, look for code smells and correct them.
  2. Once the types feel solid, begin writing outside-in tests. Test the update function logic and add unit tests as needed to fill out the tests.
  3. Once the update logic is written and tested, do the work to tie the view to the model and test at at the browser level.

This all seems so simple and clean and we all want it to be this way, but in reality, nothing is this linear (this is why waterfall development doesn’t work). As much as we like to think we can make our types perfect at the beginning, actual development will uncover things that don’t smell so good. Today I’m going to walk through an example of this situation and talk about how I like to deal with it.

Here’s the Problem

In defining my model to score a game of cricket, I first developed these types.

type Target
= Twenty
| Nineteen
| Eighteen
| Seventeen
| Sixteen
| Fifteen
| Bullseye
type TargetStatusValue
= Unopened Int
| Opened
| Closed
type alias TargetStatus =
{ target : Target
, status : TargetStatusValue
}
type alias Player =
{ name : String
, status : List TargetStatus
, score : Int
}

Here, each player has a list of scoring targets on the dart board. Each target status is a record containing the target and the status of that target. This all looks pretty straightforward and makes for a nice concise Player record. When I created this, I loved the fact that it only had three elements and all of the complexity was hidden behind that single status element.

This is certainly the approach I would have taken in an object oriented environment. I’d compose objects from other objects and try to use abstractions to hide the complexity of the underlying objects. So I largely took this approach out of habit.

The trouble started when I tried to use this model for a relatively simple operation. To follow this example, you’ll need to see how a single Player was initialized.

initStatus : List TargetStatus
initStatus =
[ { target = Fifteen
, status = UnOpened 0
}
, { target = Sixteen
, status = UnOpened 0
}
, { target = Seventeen
, status = UnOpened 0
}
, { target = Eighteen
, status = UnOpened 0
}
, { target = Nineteen
, status = UnOpened 0
}
, { target = Twenty
, status = UnOpened 0
}
, { target = Bullseye
, status = UnOpened 0
}
]
initPlayer1 : Player
initPlayer1 =
{ name = "Player 1"
, status = initStatus
, score = 0
}

Note that the status element for each player is a list of TargetStatus elements. There is one for each valid scoring target. These are initialized to the proper state for a new game.

The trouble begins when I needed to write a simple function to lookup the value of a particular Target type. I wanted to make a function with this type signature.

getTargetStatus : List TargetStatus -> Target -> TargetStatusValue

In the functional programming world, this is a pretty simple process. I want to filter the list to extract the element with the matching Target type, and extract the status for that result. I wanted to create code code that looked something like this.

getTargetStatus statusList target =
statusList
|> List.filter (\elem -> elem.target == target)
|> .status

There is a problem here. List.filter returns a list since there could be multiple matches of that Target type. This is the first clue that our types aren’t quite right. It’s possible to use these type definitions and build a list that makes no sense in the problem domain. We should never have multiple targets with the same type in this list.

Pushing on, since I knew that there was only one matching case in the real world, I could just take the first element of the resulting list.

getTargetStatus statusList target =
statusList
|> List.filter (\elem -> elem.target == target)
|> List.head
|> .status

This also fails to compile as List.head returns a Maybe type. If the resulting list is empty, Nothing will be returned. Again, I tried to rationalize this by telling myself that I as the developer could ensure the data was never invalid, so I forged on and created this code that did compile.

getTargetStatus statusList target =
statusList
|> List.filter (\elem -> elem.target == target)
|> List.head
|> Maybe.withDefault
{ target = target, status = Unopened 0}
|> .status

This does compile, but seems fairly awful. In the (supposedly illegal) case where the filter function returned an empty list, I just picked an arbitrary value to return. While this should never happen in the real-world, now if it does, my application will continue on working with bad data.

I didn’t like this at all so I thought I should have a way to record this invalid state in my model. I made these changes.

type TargetStatusValue
= Unopened Int
| Opened
| Closed
| IllegalStatus
getTargetStatus statusList target =
statusList
|> List.filter (\elem -> elem.target == target)
|> List.head
|> Maybe.withDefault
{ target = target, status = IllegalStatus }
|> .status

This makes it clear that something bad happened, but now I will have to complicate my application to account for this potential value every time I pattern match against TargetStatusValue. Even then, it’s still not clear what I do if I find this invalid state. Do I crash the application? Do I just ignore it? In any case, it’s clear that allowing this ambiguous and illegal state in my code is not going to make my future self happy.

So let’s try to refactor to remove this possibility.

Refactoring to Remove the Invalid State

The root of this problem seems to be that I decided to store the scoring possibilities in a list. This made my player object simpler and might allow me to later expand the game to support other rules. For instance, if I wanted to score a new name that allowed scoring on targets 13 and 14, I’d just have to change one structure. But in reality, I’m writing an application to score one particular dart game and I don’t even know how other dart games are scored. I’m clearly keeping open possibilities that will likely never happen. I’m violating the principle of “you ain’t gonna need it.”

I can get around this by being much more explicit and only allowing the possibilities that were really needed.

My resulting Player record looks like this.

type alias Player =
{ name : String
, status15 : TargetStatus
, status16 : TargetStatus
, status17 : TargetStatus
, status18 : TargetStatus
, status19 : TargetStatus
, statusBullseye : TragetStatus
, score : Int
}

and the initialization take this form.

initPlayer1 : Player
initPlayer1 =
{ name = "Player 1"
, status15 = Upopened 0
, status16 = Unopened 0
, status17 = Unopened 0
, status18 = Unopened 0
, status19 = Unopened 0
, status20 = Unopened 0
, statusBullseye = Unopened 0
, score = 0
}

While this record structure looks a little more unwieldy, it more accurately represents the real problem domain and avoids a multitude of issues downstream.

If this record continued to grow, and you wanted it to look a bit neater, you could refactor like this.

type alias Player =
{ name : String
, statuses : PlayerStatuses
, score : Int
}
type alias PlayerStatuses =
{ status15 : TargetStatusValue
, status16 : TargetStatusValue
, status17 : TargetStatusValue
, status18 : TargetStatusValue
, status19 : TargetStatusValue
, statusBullseye : TargetStatus
}

I’ve chosen to not make this change as updating deeply nested records in Elm can be a bit convoluted and it doesn’t feel necessary it this point.

Getting Up and Running Again

While this change is relatively simple, it doesn’t come for free. After I made this change, I had to go back and refactor my application to use this new structure. Given that I found this issue while writing the first test to use it, the changes aren’t huge. It’s largely a matter of updating the test data for my model tests to use this new structure. With the Elm compiler to help me through the process, I was able to get things updated in about 10 minutes.

One of the great things about the development process I am using is that I am not putting any effort into creating a view until I have the bulk of the business logic tested. In this way, when I find structural issues like this, I’m limited to changing update logic and tests and these are usually quite easy to fix. Since I am reacting and changing tings as soon as find an issue during testing, I usually have very small amounts of code to change.

Conclusion

As much as we want to believe that we can design our types up-front and not make mistakes, our human brains cannot understand the whole problem up-front. As we write our code, we will always encounter situations where the type design makes it difficult to write clean code. In these cases, it’s really tempting to just keep pushing along, working through the corner cases without thinking about the underlying cause of the difficulties. The more I write Elm code, the more I’m likely to explore these difficulties and try to see if a tweak to the initial type definitions might make it easier to write cleaner code.

In other languages, I’d steer away from large scale data structure changes after a certain amount of code was written. With the strong types in Elm and a compiler that ensures no mismatches, I feel much more confident fixing these fundamental issues as soon as I find them. This results in immediately cleaner code and in the future, code that is easier to change and maintain.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.