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 List
s 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 animpl
function anymore, but a newinterface
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 exampleCounterOperations 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 typer
(in your exampleCounterOperations r
).The function should return the same operations record, but now parameterized with the type
t
(in your exampleCounterOperations 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 theup
anddown
fields of theCounterOperations
record need a “raise” call, but thevalue
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
andList ()
.As these will be the interface implementations, I’d probably name them
counterImplInt
andcounterImplList
. 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 isr -> 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 ther -> 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
andCounterOperations
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 anInterface2
module and a functiondefineOperations
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 theCounter
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 functionimplement
from a possibleInterface2
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.