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 Counter
s:
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:
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 Counter
s, 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 Counter
s 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.