Demystifying Jeremy's Interfaces

It seems that I’m not able to change the table of contents in the first post anymore. If I find no other way, I’ll post the final table of contents after the last part of this series.

Intermezzo #1: Back to the Real World

I love happy ends, so it’s a perfect time to end the story of Jeremy, Rupert, and Pit.


The last thing for me to do is to apply the new interface technique to my introductory example: a list of different things. If you remember, I wanted to create a list with Strings, Ints, and Bools:

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

and then be able to apply functions like

thingSize : Thing -> Int

thingDouble : Thing -> Thing

to the list elements. The problem with the original implementation was that, in order to add yet another wrapped type, for example Char, we needed to

  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)

I promised that it’s possible to omit tasks #2 and #3, and this is exactly what Jeremy’s magic interface technique enables us to achieve.


First, we have to define the interface type and the operations:

type Thing
    = Thing (ThingOperations Thing)


type alias ThingOperations t =
    { size : Int
    , double : t
    }

I add some convenience functions for the users of the Thing type:

thingSize : Thing -> Int
thingSize (Thing ops) =
    ops.size


thingDouble : Thing -> Thing
thingDouble (Thing ops) =
    ops.double

As the designer of the interface, I have to provide:

  • the constructor function: Thing
  • a map function for the operations record:
thingOps : (r -> t) -> ThingOperations r -> ThingOperations t
thingOps raise ops =
    { size = ops.size
    , double = raise ops.double
    }

As an implementer of the interface, I have to implement the operations for my internal representation type:

thingImplString rep =
    { size = stringSize rep
    , double = stringDouble rep
    }


thingImplInt rep =
    { size = intSize rep
    , double = intDouble rep
    }


thingImplBool rep =
    { size = boolSize rep
    , double = boolDouble rep
    }

Using the three parts (constructor, map for operations record, implementation of operations), I can provide functions to create the various kinds of things:

aString : String -> Thing
aString =
    IF.createInstanceOf Thing thingOps thingImplString


anInt : Int -> Thing
anInt =
    IF.createInstanceOf Thing thingOps thingImplInt


aBool : Bool -> Thing
aBool =
    IF.createInstanceOf Thing thingOps thingImplBool

I really like the look-and-feel of the code :star_struck:

(Note that I used the interface function from the last part of the story, but renamed it to createInstanceOf. I’m still experimenting with the Interface API…)


Now I can put on the interface user’s hat and create a list of different things:

myListOfThings : List Thing
myListOfThings =
    [ aString "one", anInt 1, anInt 12, aBool True ]

Instead of the constructor functions AString, AnInt, and ABool from the former wrapper type, I now use the instance creation functions aString, anInt, and aBool. Nice and easy!

I create a small test program (here’s an Ellie with the code):

main : Html msg
main =
    myListOfThings
        |> List.map thingSize
        |> Debug.toString
        |> Html.text

I start elm reactor, navigate to my source file, and get:

Initialization Error

RangeError: Out of memory


:interrobang:


What ??? Has all this just been a fairy tale ???

No, no, no. I need a happy end! The story has to be continued…