Conversion functions, five stars
Tags: elm
Published: 2018-12-29 13:42:05

---
title: Conversion functions, five stars
published: true
description: Conversion functions don't have a great reputation, being associated with boiler plate, or boredom. I think they're secretly great, and will try to convince you too.
tags: elm
series: Using Elm types outside the Model
---

I recently found what I thought was a neat trick to simplify an update function, in an app that had a lot of update function. The trick involved creating a new type similar but different to the Model type, a strategy I'm so fond of I co-authored two previous blog posts about it, [A type for views] and [Keeping decoders simple]. So after complicated view and decoding logic, this time we'll try our hand at complicated update logic.

Afterwards I'd like to share where my enthusiasm for creating similar types is coming from. Sneak preview: it's conversion functions. Turns out I love conversion functions.

## An example application

Suppose we're building an app to support humanity's effort to populate the solar system[^1]. It should keep track of the populations of all the planets in the solar system, as well as any ships in orbit. Here's a model for that application:

```elm
type alias Model =
    Body


type Body
    = Body
        { name : Name
        , population : Int
        , moons : List Body
        }


type alias Name =
    String


solarSystem : Model
solarSystem =
    Body
        { name = "Sun"
        , population = 0
        , moons =
            [ Body
                { name = "Earth"
                , population = 7700000000
                , moons =
                    [ Body
                        { name = "Moon"
                        , population = 0
                        , moons = []
                        }
                    , Body
                        { name = "Worf's starship"
                        , population = 1
                        , moons = []
                        }
                    ]
                }
            , Body
                { name = "Mars"
                , population = 0
                , moons = []
                }
            ]
        }
```

As you see we don't have a complete solar system here, but it's enough to illustrate the two operations the app needs to support:
- We want to move ships from one planet to another.
- We want to adjust the populations of these planets.

Here's a `Msg` type for that:

```
type Msg
    = SetPopulation Name Int
    | Move { body : Name, destinationOrbit : Name }
```

So far so good. Now we get to implement the update function and this is where things become unpleasant. To handle the `SetPopulation` message, we will need to search through the entire solar system to find the planet we need to update. Handling `Move` is going to be even harder: Consider what steps we need to take to move Worf to Mars:

1. First we need to find Worf somewhere in the solar system and store him in a variable.
1. Then we need to find the body Worf is orbiting, and remove Worf as a moon of that body.
1. And last we need to find the body Worf is moving to, and insert the copy of Worf we stored as a new moon of that body.

Oh, one last thing, should it turn out that destination body doesn't exist, we should cancel the entire operation. Otherwise Worf would disappear from the solar system entirely.

## A simple `update` function

When we break it down like that, it sounds like this logic will get pretty complicated. So before committing to this approach let's try to consider different solar system types. Does one exist that would make handling the messages described above simple? Yes, there is! Check out the following type for one possible approach:

```elm
type alias FlatModel =
    Dict Name FlatBody


type alias FlatBody =
    { orbiting : Maybe Name
    , population : Int
    }
```

To proof that updating this type really is easier, here's the implementation of a `setPopulationOf` and `moveBodyTo` function:

```elm
setPopulationOf : Name -> Int -> FlatModel -> FlatModel
setPopulationOf name newPopulation flatModel =
    Dict.update
        name
        (Maybe.map (setPopulation newPopulation))
        flatModel


setPopulation : Int -> FlatBody -> FlatBody
setPopulation newPopulation flatBody =
    { flatBody | population = newPopulation }


moveBodyTo : Name -> Name -> FlatModel -> FlatModel
moveBodyTo movingBody destinationOrbit flatModel =
    Dict.update
        movingBody
        (Maybe.map (moveBody destinationOrbit))
        flatModel


moveBody : Name -> FlatBody -> FlatBody
moveBody destinationOrbit flatBody =
    { flatBody | orbiting = Just destinationOrbit }
```

As you can see we don't require three-step-plans to move Worf to another planet. Both `setPopulationOf` and `moveBodyTo` update a single value in a dictionary -- that's it! Given this triump of `FlatModel` over `Model`, should we just dump `Model` entirely and only work with `FlatModel`? Maybe for this toy example that wouldn't be such a bad idea, but there's definitely downsides to doing this.

One such downside is that the `FlatModel` type doesn't enforce each body is orbiting another body that exists. We can define Worf as `{ population = 1, orbiting = Just "Teapot" }`, without there being `Teapot` in the solar system. Put another way, `FlatBody` allows more 'impossible states' then `Body`, and as a rule it's nice to avoid these when we can.

Another disadvantage is choosing a Model type to simplify our update logic can complicate our view logic. It's not inconceivable the tree structure of the original `Model` will prove convenient for drawing: We will be able to first draw the sun, then the earth around it, then the moon around the earth, etc. The `FlatModel` doesn't provide us the planetary bodies in a useful order. So changing the `Model` type will simplify the update logic, but maybe at the expense of complicating the `view` logic.

Ideally we'd like to use both types for what they're good at. We keep our `Model` type, so we prevent a bunch of impossible states, but for updates we use our `FlatModel` type which is easier to manipulate. We can do that, but we'll need to conversion functions between both types. For now I'll just give the types of these conversion functions:

```elm
toFlatModel : Model -> FlatModel
toFlatModel = Debug.todo "We'll need to implement this function."

fromFlatModel : FlatModel -> Maybe Model
fromFlatModel = Debug.todo "And this function as well."
```

You may notice that `fromFlatModel` returns a `Maybe`. This is because, as we saw before, `FlatModel` allows certain impossible states. We can't convert such a state to a `Model` which doesn't allow these states, so we'll return `Nothing` in that case.

Once we implement these conversion functions, we can use them to implement our `update` function.

```elm
update : Msg -> Model -> Model
update msg model =
  case msg of
      SetPopulation body newPopulation ->
        toFlatModel model
          |> setPopulationOf body newPopulation
          |> fromFlatModel
          |> Maybe.withDefault model
      Move { body, destinationOrbit }
        toFlatModel model
          |> moveBodyTo body destinationOrbit
          |> fromFlatModel
          |> Maybe.withDefault model
```

And with that we're almost done. The one thing left to do is to implement those conversion functions. I'm not going to print those here because of their length, but you can check them out in [the full code] of this post.

## Was it worth it?

We achieved what we set out to do: we have a simple update function without compromising the strength of our `Model` type. For this we payed with the need to create an additional `FlatModel` type and two conversion functions. Was that price worth the gain?

Maybe the use of two different types for the same concept sets of some warnings sounds. It's repetition of a sort, something we're often told to avoid in order to achieve maximum code re-use. You'll have to make your own decision, but I'll trade a reduction of code complexity for an additional type any day.

But did we reduce code complexity? We ended up writing two new conversion functions that aren't exactly simple themselves. Maybe it's more honest to say we moved complexity around the app, away from the update functions and into these conversion functions. I still call that a win, because I believe conversion functions are the best kind of complicated code you can have. To support that point I'd like to take the final part of this post to look at the benefit of conversion functions.

## Pitching conversion functions

Let's take a look at some nice properties of conversion functions.

**Conversion functions don't leak their problems.** Conversion functions can be messy, but at worst they are an [Omega mess], to use a term coined by Sandi Metz. No matter how messy the conversion function, it's always confined between these two types it converts between. There's no risk of the mess spilling over into other parts of the application, resulting in spaghetti code. I find that a comforting thought.

**Conversion functions are easy to write.** The Elm compiler is at its most helpful when you're writing conversion functions. It doesn't know anything about solar systems, or any other domain, so it can't help you much with that. If we by accident clone Worf instead of moving him, that's not the type of error the compiler picks up on. But conversion functions, that take one specific type and return another, are something the Elm compiler really gets. In our example application we had a lot of conversion code and only a small amount of domain-manipulation code, and so we've optimized our app to play to the compiler's strengths.

**Conversion functions are easy to test.** If you ever wondered what would be a good place to use the functions in `elm-test`'s `Fuzz` module: this is it. Conversion functions change the way data is represented without changing the meaning of that data. For example: the conversion functions in the solar system example don't change which planets exist, or their populations. We can test that these 'rules' hold for any random solar system we generate.

The solar system example allows an even nicer test, because we have one conversion function each way: We can check that if we convert a random solar system one way and then back, we always end up with the same solar system we started with. A single such 'roundtrip test' will easily cover more failure scenario's then you would come up with yourself.

**Writing conversion functions is a trainable skill.** The conversion functions for the example in this post aren't simpler than the complicated update logic they would replace. But as we write more conversion functions, we'll start to recognize we're applying the same couple of tricks over and over again. Business logic will be different from one app to the next, but the writing of conversion function is a cross-domain skill that can be trained.

## That's a wrap

We've seen an example of pushing code complexity into conversion functions, and I've tried to sell you on this strategy. Thank you for reading! I'd love to hear about your own experiences, good and bad, writing conversion functions.

[^1]: Elm 0.19 introduced some nice support for interplanetary Elm programmers: it supports local elm package caching!

[A type for views]: https://dev.to/noredink/a-type-for-views-5g04
[Keeping decoders simple]: https://dev.to/jwoudenberg/keeping-decoders-simple-360e
[the full code]: https://ellie-app.com/4j2g3JjhPVva1
[Omega mess]: https://vimeo.com/48106365