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
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 caseList ()
, into the mainCounter
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…