Demystifying Jeremy's Interfaces

Thank you, Rupert, for your buffer example. You are a little bit further in your code than I am here, but I’ll catch up in this part.


Jeremy’s Original Code

So, after the (maybe not so interesting) introduction, let’s proceed straight to the example from Jeremy’s talk. I recommend reading his slides first, because I don’t repeat his introduction here.

First, he defines two mutually recursive types:

type Counter
    = Counter CounterRecord


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

This defines an “interface” named Counter together with the functions up, down, and value, all three combined in the CounterRecord.

There certainly are similarities between Jeremy’s Counter and my introductory Thing example:

Jeremy Pit
Exposed type Counter Thing
Exposed functions up, down, value thingSize, thingDouble
Underlying types Int, List () String, Int, Char

But there are differences. The most important one: in Jeremy’s example, you don’t see the underlying types at all. In fact, I mentioned the underlying types Int and List () just because Jeremy used them in his examples, as we’ll see soon, but there could be many more. The definition of the “interface” is totally independent of the actual “implementations”!


Great, but now the question is: how can I create such a Counter?

This is where the magic begins. Here’s Jeremy’s code to create two kinds of Counters:

intCounter : Int -> Counter
intCounter =
    impl CounterRecord
        |> wrap (\raise rep n -> raise (rep + n))
        |> wrap (\raise rep n -> raise (rep - n))
        |> add identity
        |> map Counter
        |> init (\raise rep -> raise rep)


listCounter : Int -> Counter
listCounter =
    impl CounterRecord
        |> wrap (\raise rep n -> raise (List.repeat n () ++ rep))
        |> wrap (\raise rep n -> raise (List.drop n rep))
        |> add List.length
        |> map Counter
        |> init (\raise n -> raise (List.repeat n ()))

Both functions have the same signature: Int -> Counter. They return a Counter with the given Int as the starting value. An intCounter allegedly uses the type Int internally to represent the counter state, a listCounter uses the type List (), with the length of the list representing the counter state. We’ll look at the details of these functions in the next parts…

Here are the helper functions that make it all possible:

impl : t -> (raise -> rep -> t)
impl constructor =
    \_ _ -> constructor


wrap : (raise -> rep -> t) -> (raise -> rep -> (t -> q)) -> (raise -> rep -> q)
wrap method pipeline raise rep =
    method raise rep |> pipeline raise rep


add : (rep -> t) -> (raise -> rep -> (t -> q)) -> (raise -> rep -> q)
add method pipeline raise rep =
    method rep |> pipeline raise rep


map : (a -> b) -> (raise -> rep -> a) -> (raise -> rep -> b)
map op pipeline raise rep =
    pipeline raise rep |> op


init : ((rep -> sealed) -> flags -> output) -> ((rep -> sealed) -> rep -> sealed) -> flags -> output
init initRep pipeline flags =
    let
        raise : rep -> sealed
        raise rep =
            pipeline raise rep
    in
    initRep raise flags

As I’ve already written, at this point in the talk my reaction was simply:

:interrobang:


Before trying to understand how this code works, I wanted to check whether it works at all.

Sidemark: the German translation of “to understand” is “begreifen”. This word contains another German verb: “greifen”, which means “to grab” or “to take hold of”. I like this connection, because in order to understand something it often helps if I can grab it, touch it and play with it.

We have functions to create Counters, but the functions in the CounterRecord look a little bit unfamiliar. For example, for a function like up, which increases a Counter, I’d expect a signature like

up : Int -> Counter -> Counter

but in the CounterRecord we have the function

up : Int -> Counter

At this place, it helps to have some OO knowledge. In an object-oriented programming language, for example in Kotlin, we could define a Counter interface like this:

interface Counter {
    fun up(n: Int): Counter
    fun down(n: Int): Counter
    val value: Int
}

Note that here we have exactly the same function signatures as in Jeremy’s CounterRecord!

We continue with the OO example: Whenever I have an object which “implements” the Counter interface, then I can call the defined methods on this object. For example, in Kotlin, I could write a function to increment a Counter like this:

fun increment(counter: Counter): Counter =
    counter.up(1)

The function increment gets an object named counter which implements the Counter interface, and returns a new Counter object. Internally, it calls the up method on the counter object using the counter.up(1) syntax.

In Elm, the increment function could be defined (surprisingly similar) like this:

increment : Counter -> Counter
increment (Counter counterRecord) =
    counterRecord.up 1

Takeaway: Whenever I would call a method on an object implementing an interface in the OO world, in Elm, with Jeremy’s interfaces, I destructure the value to get the record with the functions and then call the appropriate one.

This allows me to implement some more Elm-like Counter functions:

up : Int -> Counter -> Counter
up n (Counter counterRecord) =
    counterRecord.up n


down : Int -> Counter -> Counter
down n (Counter counterRecord) =
    counterRecord.down n


value : Counter -> Int
value (Counter counterRecord) =
    counterRecord.value

And now I can start to play with Jeremy’s code.

Let’s create a list of different Counter types with different starting values:

myListOfCounters : List Counter
myListOfCounters =
    [ intCounter 1, listCounter 2, intCounter 11, listCounter 12 ]

Now we can check whether they were created with the correct initial values:

List.map value myListOfCounters
--> [ 1, 2, 11, 12 ]

Yeah, it seems to work!

Let’s increment them and then get the new values:

myListOfCounters
    |> List.map (up 3)
    |> List.map value
--> [ 4, 5, 14, 15 ]

And now decrement:

myListOfCounters
    |> List.map (down 3)
    |> List.map value
--> [ -2, 0, 8, 9 ]

Hmm. At first glance, the second value looks suspicious. “2 - 3” should be “-1” and not “0”. At second glance, the second and fourth Counters have been created with the function listCounter, so they internally use a value of type List (), and the length of the list is used to represent the current state of the counter. Since the length of a list can’t be negative, the implementation of listCounter isn’t able to represent negative values. The lowest value we can get is “0”.


Great. The code works as expected!

In case you want to play with the code, too, Jeremy posted an Ellie with the code from his slides, and here’s an Ellie with my test code so far.

In the next part, we’ll start to get a better understanding of Jeremy’s code as we begin to modify it a little bit.

5 Likes