Demystifying Jeremy's Interfaces

Change #3: Separate Interface from Implementation

Remember that the characters in the story are purely fictional…


Time has passed, and Rupert is using interfaces in more and more places. He even wrote a blog post about it. Reflected as he is, he notices that his role in the interface business has shifted recently. He is mainly defining the interfaces, leaving the implementation to others, for example to Pit, who loves to use Lists for everything, just as in the listCounter example.

Just like Jeremy managed to hide the “raise” function from the users of his init function in the last part, Rupert would love to hide the “raise” function from the folks implementing his interfaces.

Applying the “raise” function is something which has to be repeated in each interface implementation:

intCounter : Int -> Counter
intCounter start =
    impl Counter
        (\raise rep ->
            { up = \n -> raise (rep + n)
            , down = \n -> raise (rep - n)
            , value = rep
            }
        )
        start


listCounter : Int -> Counter
listCounter start =
    impl Counter
        (\raise rep ->
            { up = \n -> raise (List.repeat n () ++ rep)
            , down = \n -> raise (List.drop n rep)
            , value = List.length rep
            }
        )
        (List.repeat start ())

Wouldn’t it be nice, if he could somehow hide the call of the “raise” function, so that the code would look more like

intCounter : Int -> Counter
intCounter start =
    impl Counter
        (\rep ->
            { up = \n -> rep + n
            , down = \n -> rep - n
            , value = rep
            }
        )
        start


listCounter : Int -> Counter
listCounter start =
    impl Counter
        (\rep ->
            { up = \n -> List.repeat n () ++ rep
            , down = \n -> List.drop n rep
            , value = List.length rep
            }
        )
        (List.repeat start ())

As always in a situation like this, he calls Jeremy and tells him about his new idea. Jeremy is skeptical at first, but promises to get back to Rupert.

Indeed, just a few days later, Jeremy calls back and says:

Jeremy

Rupert, I’m sorry, but this isn’t something I can do for you, because there’s no general way to define this behavior for each and every interface. But if you are willing to tell me which fields of the operations record need a “raise” call and which don’t, then I can change the impl function in a way that lets you completely hide the “raise” function from the implementers of your interfaces.

Of course, Rupert would gladly provide this information to the Interface module, so Jeremy sends him the new version of the module and guides him in the process to transform Rupert’s existing code to the new version.

Jeremy

OK Rupert, let’s start: first, you need to add a type parameter to the operations record. And could you rename the operations record from CounterRecord to, say, CounterOperations? In my mind they are always interface “operations”…

Rupert is fine with the name change. After all, he doesn’t know yet how the code will look like in the end, so he trusts Jeremy that the new name will be well chosen. The new types look like this:

type Counter
    = Counter (CounterOperations Counter)


type alias CounterOperations t =
    { up : Int -> t
    , down : Int -> t
    , value : Int
    }

Jeremy

Now you have to use a new function from the Interface module. The new version of the module doesn’t expose an impl function anymore, but a new interface method. You’ll see why I renamed the function, soon.

Jeremy’s new method has the following signature:

interface :
    (ot -> t)
    -> ((r -> t) -> or -> ot)
    -> (r -> or)
    -> (r -> t)

Rupert has worked with Jeremy’s code long enough to easily map the abstract type names to his concrete use cases. Here’s the mapping for the Counter example:

Abstract Concrete
t Counter
r hidden type, for example Int or List ()
ot CounterOperations Counter
or CounterOperations r

Rupert

Ok, Jeremy, I can translate the types to the counter example. What should I do with the function?


Jeremy

You as the interface designer should publish the interface function partially applied with the first two parameters.

Oh-K… Rupert looks at the first two parameters in more detail.

Rupert

The first parameter of type ot -> t (in my example CounterOperations Counter -> Counter) is the constructor function (Counter).


Jeremy

Right. I can tell you a little bit about the second parameter of type (r -> t) -> or -> ot.

It is a function that takes the well-known “raise” function of type r -> t and an operations record, parameterized with the type r (in your example CounterOperations r).

The function should return the same operations record, but now parameterized with the type t (in your example CounterOperations Counter).

So, basically, this is just a map function on the operations record.


Rupert

OK, a map function seems to be easy.


Jeremy

If I might add another naming proposal: I’d name the partially applied function “xxxInterface”.

Rupert writes the following counterInterface function:

counterInterface =
    interface Counter <|
        \raise ops ->
            { up = ops.up >> raise
            , down = ops.down >> raise
            , value = ops.value
            }

His IDE shows that the function signature is

counterInterface : (r -> CounterOperations r) -> (r -> Counter)

Rupert

Hey, I’m starting to understand how your new interface function helps to separate the interface definition from the implementation:

The second parameter tells the Interface module that the up and down fields of the CounterOperations record need a “raise” call, but the value field doesn’t. This is exactly the information I wanted to hide from the interface implementers.

Plus, I think I also understand why you named the function “interface”: the interface designers use it to define their interfaces.

So what can the interface implementers do with my counterInterface function?


Jeremy

They need to provide the next parameter, the one with type r -> CounterOperations r. This implements the counter operations for one specific internal representation type.


Rupert

No problem. I can easily extract the logic from the previous implementations for Int and List ().

As these will be the interface implementations, I’d probably name them counterImplInt and counterImplList. What do you think?


Jeremy

The names sound great. Go ahead.

Rupert writes the following implementations:

counterImplInt : Int -> CounterOperations Int
counterImplInt rep =
    { up = \n -> rep + n
    , down = \n -> rep - n
    , value = rep
    }


counterImplList : List () -> CounterOperations (List ())
counterImplList rep =
    { up = \n -> List.repeat n () ++ rep
    , down = \n -> List.drop n rep
    , value = List.length rep
    }

Rupert

Yes! This is exactly what I wanted to achieve: the implementers just have to be concerned with their internal type and nothing else. No need to call a “raise” function. All they need to do is to implement the interface operations using their internal type.


Jeremy

You’ve got it.

Now look again at the signature of the interface function. The last remaining part is r -> t.

When the implementers pass one of the “implementation” functions to the provided “interface” function, they get a function of type r -> t, which lets them create “objects” implementing the interface.


Rupert

Let’s just do this for the listCounter example. I really want to see the final result.

He writes:

listCounter : Int -> Counter
listCounter start =
    counterInterface counterImplList <|
        List.repeat start ()

Rupert

It compiles!

What do you think? Should I reverse the order? Let’s see…

intCounter : Int -> Counter
intCounter start =
    start
        |> counterInterface counterImplInt

Rupert

I’ll have to play with this a little bit more. At least the code is very modular now. Thank you very much, Jeremy.


Jeremy

I’m glad I could help you.

If you’re not sure about the “look-and-feel” of the code: there are many more options to structure the code. The only important thing is that you somehow collect the three values needed by the interface function and that you know how to combine them to get the r -> t function in the end.


Rupert

Ok, let me summarize the three values…

Rupert writes:

Type Contents
ot → t Constructor function
(r → t) → or → ot “map” function for the operations record
r → or Implementation of the interface operations

Rupert

Done. But how could I structure the code differently?


Jeremy

I’ll show you an example soon, but let’s first look at the implementation of the interface function to see how these three values can be combined:

interface : (ot -> t) -> ((r -> t) -> or -> ot) -> (r -> or) -> (r -> t)
interface ott rtorot ror =
    let
        rt : r -> t
        rt r =
            ott (rtorot rt (ror r))
    in
    rt

With the abstract names representing the types, it’s easy to visually type-check the code. The implementation of this new function looks very similar to the implementation of the previous impl function. Jeremy’s magic shines again!


Jeremy

OK, with that out of the way, here’s another possible syntax / DSL for the counter interface example:

The Counter and CounterOperations types stay the same as above.

The interface designer could supply her information in this way:

counterOperations : IF2.Operations r Counter (CounterOperations r) (CounterOperations Counter)
counterOperations =
    IF2.defineOperations <|
        \raise ops ->
            { up = ops.up >> raise
            , down = ops.down >> raise
            , value = ops.value
            }

Jeremy

I’m using an opaque type Operations here which could be defined in an Interface2 module and a function defineOperations to create such a value. I can show you their implementation later.

I’m sure you see what kind of value this type holds?


Rupert

Of course. It’s the “map” function for the CounterOperations record used to “raise” the parameterized record from the internal representation type to the Counter type.


Jeremy

Right. The interface implementers could use this to add their knowledge, too:

counterImplInt : IF2.Implementation Int Counter (CounterOperations Int) (CounterOperations Counter)
counterImplInt =
    IF2.implement counterOperations <|
        \rep ->
            { up = \n -> rep + n
            , down = \n -> rep - n
            , value = rep
            }


counterImplList : IF2.Implementation (List ()) Counter (CounterOperations (List ())) (CounterOperations Counter)
counterImplList =
    IF2.implement counterOperations <|
        \rep ->
            { up = \n -> List.repeat n () ++ rep
            , down = \n -> List.drop n rep
            , value = List.length rep
            }

Jeremy

Again, I’m using an opaque type Implementation and a function implement from a possible Interface2 module.


Rupert

Nice. Here you add the type-specific implementations of the interface operations.

But didn’t we miss the first value that is needed, the type constructor function?


Jeremy

I left it for the last part where we put all three things together. If the constructor function is named like the interface type, in your case Counter for both the type and the constructor, then the following code reads nicely:

intCounter : Int -> Counter
intCounter start =
    IF2.createInstanceOf Counter <|
        IF2.implementedBy counterImplInt <|
            IF2.fromValue <|
                start


listCounter : Int -> Counter
listCounter start =
    IF2.createInstanceOf Counter <|
        IF2.implementedBy counterImplList <|
            IF2.fromValue <|
                List.repeat start ()

Jeremy

I know that this is a very contrived example, but I wanted to show you what is possible.


Rupert

Ok, ok. This reads very nicely, indeed. I’m not sure I like the syntax with the many backward pipe operators, but I understand what you wanted to teach me: that we have many ways to design the interface API.

Can you show me the implementation of those types and functions?


Jeremy

Sure. Here they are:

type Operations r t or ot
    = Operations ((r -> t) -> or -> ot)


type Implementation r t or ot
    = Implementation
        { rtorot : (r -> t) -> or -> ot
        , ror : r -> or
        }


type Instance r t or ot
    = Instance
        { rtorot : (r -> t) -> or -> ot
        , ror : r -> or
        , r : r
        }


type Value r
    = Value r


defineOperations :
    ((r -> t) -> or -> ot)
    -> Operations r t or ot
defineOperations =
    Operations


implement :
    Operations r t or ot
    -> (r -> or)
    -> Implementation r t or ot
implement (Operations rtorot) ror =
    Implementation
        { rtorot = rtorot
        , ror = ror
        }


createInstanceOf :
    (ot -> t)
    -> Instance r t or ot
    -> t
createInstanceOf ott (Instance inst) =
    let
        rt : r -> t
        rt r =
            ott (inst.rtorot rt (inst.ror r))
    in
    rt inst.r


implementedBy :
    Implementation r t or ot
    -> Value r
    -> Instance r t or ot
implementedBy (Implementation impl) (Value r) =
    Instance
        { rtorot = impl.rtorot
        , ror = impl.ror
        , r = r
        }


fromValue :
    r
    -> Value r
fromValue =
    Value

Rupert

Oh, wow. I have to look at this in more detail, but I think I see how you use the types to gradually collect the needed values and finally put them all together in the… wait… the createInstanceOf function.

I’m sure I could change the DSL syntax, for example, to use forward pipe operators now.

Once again: thank you, Jeremy. You gave me a lot to play with.


Jeremy

Once again: I’m glad I could help. Feel free to ask me if you get stuck.


Is this the end of the story? A happy end?

Maybe… maybe not…

Here’s an Ellie with the new code and another one with the DSL like syntax.

3 Likes