Demystifying Jeremy's Interfaces

Change #1: Remove the Record Constructor Function

In this part, we’ll start to look at the code from different angles: it could be from the point of view of the author of the interface technique, or from a user’s point of view.

While lying in my bed this morning, pondering about what to write in this part, I thought: why don’t you introduce some fictitious characters for those different viewpoints? It felt interesting, so let’s try it. So far, I have been simply analyzing the code. From now on, I’ll be more like telling a story. (I split the table of contents at the beginning of this series into the appropriate sections.) So let’s start our story. Of course, any similarities with existing persons are purely coincidental :wink:


The first character is Jeremy. Jeremy is a real type system wizard. He manages the magic internal functions we’ve analyzed in the last part. Jeremy packaged the functions and the type alias W in a neat module called Interface:

module Interface exposing (W, andAdd, andMap, init, map, succeed)

The next character, let’s name him Rupert, is a user of Jeremy’s Interface module. He’s very versed in applying the interface technique to various use cases, be it buffers, geometric shapes, or recently counters. Here’s his Counter code again:

import Interface as IF


type Counter
    = Counter CounterRecord


type alias CounterRecord =
    { up : Int -> Counter
    , down : Int -> Counter
    , value : Int
    }


intCounter : Int -> Counter
intCounter =
    IF.succeed CounterRecord
        |> IF.andMap (\raise rep n -> raise (rep + n))
        |> IF.andMap (\raise rep n -> raise (rep - n))
        |> IF.andAdd (\rep -> rep)
        |> IF.map Counter
        |> IF.init (\raise n -> raise n)


listCounter : Int -> Counter
listCounter =
    IF.succeed CounterRecord
        |> IF.andMap (\raise rep n -> raise (List.repeat n () ++ rep))
        |> IF.andMap (\raise rep n -> raise (List.drop n rep))
        |> IF.andAdd (\rep -> List.length rep)
        |> IF.map Counter
        |> IF.init (\raise n -> raise (List.repeat n ()))

(Note that we re-introduced the original type and function names, because as a user, Rupert’s main interest is the domain-specific logic.)

Overall, Rupert is fine with this code, but there’s something that bothers him. Lue, a friend of Rupert, keeps telling him:

Rupert, you never should use these record type alias constructor functions, never ever!

Rupert understands his friend’s concern. He knows that, because of the problematic usage of the CounterRecord function in the IF.succeed lines, the intCounter and listCounter implementations are dependent on the field order in CounterRecord. Rupert is responsible enough to not change the field order without good reason, but he is not the only one working on the code. For example, there’s his new colleague, Pit. Rupert knows that Pit loves to touch and to modify every single piece of code, whether he understands it or not, just to get a better feeling for it.

There’s no doubt that one day Pit will find the CounterRecord definition. If he changes the field order to something like

type alias CounterRecord =
    { value : Int
    , up : Int -> Counter
    , down : Int -> Counter
    }

it would be no problem, because the Elm compiler would show him where the other code had to be changed, too. But if he just swaps the up and down fields as in

type alias CounterRecord =
    { down : Int -> Counter
    , up : Int -> Counter
    , value : Int
    }

the code would still compile, and maybe Pit would check in his change, despite the fact that their app wouldn’t work as before.

So Rupert looks at the implementation of the listCounter function again, trying to understand how the problematic CounterRecord constructor function is used, and whether he can do anything against it. He looks at the types that every step in the pipeline returns, just as we did in the last part of the series, and he notices that the type after the IF.andAdd call looks interesting:

listCounter : Int -> Counter
listCounter =
    IF.succeed CounterRecord
        |> IF.andMap (\raise rep n -> raise (List.repeat n () ++ rep))
        |> IF.andMap (\raise rep n -> raise (List.drop n rep))
        |> IF.andAdd (\rep -> List.length rep)
        -- IF.W (List ()) Counter CounterRecord
        |> IF.map Counter
        |> IF.init (\raise n -> raise (List.repeat n ()))

The type is

IF.W (List ()) Counter CounterRecord

which is just a shorthand for

(List () -> Counter) -> List () -> CounterRecord

Here, CounterRecord is the record type, not the problematic record constructor function his friend Lue is talking about. Rupert says to himself:

Couldn’t I directly create such a value myself?

The type says, that it needs to be a function taking two parameters:

  • a function (typically called “raise” in the listCounter code) which can turn the internal representation of the counter state, in this case List (), into the main Counter type
  • the current counter state (normally called “rep”), in the form of the internally used List ()

The function should return a CounterRecord. So Rupert starts with:

firstFourSteps : (List () -> Counter) -> List () -> CounterRecord
firstFourSteps raise rep =
    ...

He needs to return a record with the fields up, down, and value, and suddenly the function body just flows from his fingers:

firstFourSteps : (List () -> Counter) -> List () -> CounterRecord
firstFourSteps raise rep =
    { up = \n -> raise (List.repeat n () ++ rep)
    , down = \n -> raise (List.drop n rep)
    , value = List.length rep
    }

This function type-checks! Rupert doesn’t want to keep the code in the form of a top-level function, so he just takes the function body and uses it to replace the first four steps of the pipeline:

listCounter : Int -> Counter
listCounter =
    (\raise rep ->
        { up = \n -> raise (List.repeat n () ++ rep)
        , down = \n -> raise (List.drop n rep)
        , value = List.length rep
        }
    )
        |> IF.map Counter
        |> IF.init (\raise n -> raise (List.repeat n ()))

He runs a few tests, and the code works! Rupert calls Jeremy, the author of the Interface module, and tells him what he changed in his code and why he did it.


Jeremy, being the type system wizard he is, quickly recognizes the potential of Rupert’s new way to use the Interface module. He not only could completely remove the succeed, andMap, and andAdd functions, but even go one step further: the next step in the user’s pipeline would always be

        |> IF.map C

with C being the constructor function of the type

type T
    = C O

(As a type system wizard, Jeremy prefers to use the abstract names…)

Jeremy is a friendly wizard who always wants the best for the users of his module, so he’ll save his users from having to write the IF.map step every time. He removes the map function from the exposes list of the Interface module, too, and exposes a new function instead. For this function he chooses a name which is still well-known to his users. In a previous version of the module, the success function has been named impl to signal the start of the implementation of an interface.

This is the signature of the new function:

impl : (o -> t) -> W r t o -> W r t t

Jeremy really is a genius! This is the same signature as that of the previous map function, with slightly renamed type variables to better reflect the desired usage. By simply renaming the map function, he enables his users to replace the first lines of an interface pipeline with

    IF.impl C
        (\rt r ->
            ...
        )

He publishes a new version of the interface module and tells Rupert how to use it.


Here’s Rupert’s new code for the intCounter and listCounter functions:

intCounter : Int -> Counter
intCounter =
    IF.impl Counter
        (\raise rep ->
            { up = \n -> raise (rep + n)
            , down = \n -> raise (rep - n)
            , value = rep
            }
        )
        |> IF.init (\raise n -> raise n)


listCounter : Int -> Counter
listCounter =
    IF.impl Counter
        (\raise rep ->
            { up = \n -> raise (List.repeat n () ++ rep)
            , down = \n -> raise (List.drop n rep)
            , value = List.length rep
            }
        )
        |> IF.init (\raise n -> raise (List.repeat n ()))

I don’t know about you, but I very much like how this code reads. Here’s an Ellie with the actual code.


In the evening, Rupert meets Lue in a bar and proudly tells him that he not only managed to completely remove record alias constructors from their codebase, but also successfully added Lue’s elm-review rule which forbids record type alias constructors to the set of their project’s elm-review rules.

Later at home, he falls asleep relieved, knowing that even his colleague Pit can’t break the code simply by changing the order of the CounterRecord fields.


Meanwhile, Jeremy, the type system wizard, has a new idea for improving his Interface module even further…

5 Likes