Demystifying Jeremy's Interfaces

Motivation: A List of Different Things

(This introduction will be too easy for most of you. Sorry for this. We’ll come back to this example after looking at more interesting code.)


On the “#beginners” channel of the Elm Slack, people often ask questions like:

How can I create a list of different things, for example a list with strings and integers?

In other languages it isn’t a problem to define a type like string|number (TypeScript) and create a list of it, but not so in Elm. The obvious answer for Elm is:

You have to wrap them in a custom type.

For example like in

type Thing
    = AString String
    | AnInt Int

Now we can create a list of Things:

myListOfThings : List Thing
myListOfThings =
    [ AString "one", AnInt 1, AnInt 12 ]

Fine. But why might one want to create a list of different things? What can I do with such a list?

One possible reason could be:

To be able to perform the same actions on the elements, independent of their underlying type.


Let’s say we want to get the “sizes” of the Things in our list. If we had a function like

thingSize : Thing -> Int

we could simply use List.map:

List.map thingSize myListOfThings

Such a function is easy to implement: we just use a case and then handle each type in its own branch:

thingSize : Thing -> Int
thingSize thing =
    case thing of
        AString string ->
            stringSize string

        AnInt int ->
            intSize int

For the “size” of a String we choose its length:

stringSize : String -> Int
stringSize string =
    String.length string

For an Integer, we choose the number of bits it takes to represent the number:

intSize : Int -> Int
intSize int =
    if int < 2 then
        1

    else
        1 + floor (logBase 2 (toFloat int))

Now we can get the sizes of the things in our list:

List.map thingSize myListOfThings
--> [ 3, 1, 4 ]

What about modifying a wrapped type? Say we want to “double” our Things:

thingDouble : Thing -> Thing

Again, we use a case and call the type-specific functions, but in this case we have to post-process the type-specific results with the appropriate wrapping constructor:

thingDouble : Thing -> Thing
thingDouble thing =
    case thing of
        AString string ->
            stringDouble string |> AString

        AnInt int ->
            intDouble int |> AnInt

“Doubling” a string and an int:

stringDouble : String -> String
stringDouble string =
    string ++ string

intDouble : Int -> Int
intDouble int =
    2 * int

Now we can double the list elements all at once and get their new sizes:

myListOfThings
    |> List.map thingDouble
    |> List.map thingSize
--> [ 6, 2, 5 ]

So far, so good. What if we want to support Bools too?

We have to add another subtype to the wrapper type:

type Thing
    = AString String
    | AnInt Int
    | ABool Bool

Now we can add wrapped Bools to the list:

myListOfThings : List Thing
myListOfThings =
    [ AString "one", AnInt 1, AnInt 12, ABool True ]

We also need a size and a double function for Bools. Here are two contrived implementations just to see whether something is happening:

boolSize : Bool -> Int
boolSize bool =
    if bool then
        1

    else
        0


boolDouble : Bool -> Bool
boolDouble bool =
    not bool

And we need to use the Bool functions in the wrapper functions:

thingSize : Thing -> Int
thingSize thing =
    case thing of
        AString string ->
            stringSize string

        AnInt int ->
            intSize int

        ABool bool ->
            boolSize bool


thingDouble : Thing -> Thing
thingDouble thing =
    case thing of
        AString string ->
            stringDouble string |> AString

        AnInt int ->
            intDouble int |> AnInt

        ABool bool ->
            boolDouble bool |> ABool

Now we can get the sizes of a list containing Bools as well:

List.map thingSize myListOfThings
--> [ 3, 1, 4, 1 ]

And we can “double” them and get the new sizes, too:

myListOfThings
    |> List.map thingDouble
    |> List.map thingSize
--> [ 6, 2, 5, 0 ]

So, to recap: what do we have to do to add yet another wrapped type, for example Char?

  1. Implement the type-specific functions (charSize, charDouble)
  2. Add the new subtype to the wrapper type
  3. Add a new case branch to the wrapper functions (thingSize, thingDouble)

There’s no way in Elm to get around task #1. We cannot automatically “derive” size and double functions for a new type. But what if we could omit tasks #2 and #3? Sounds magical?

It is, and it needs magic, the magic of Jeremy’s Interface module!

Stay tuned…

3 Likes