Thought Experiment: Interfaces in APIs
We’re still in story-telling mode…
Rupert is thinking about new use cases for the interface technology. The technology itself can be used to hide an internal type behind an interface. But we can make the interface type itself opaque and hide the usage of the whole interface technology behind a module API.
Rupert wants to try this with a real example, and it doesn’t take him long to implement something. He shows the new module to his colleague Pit.
Rupert
Hey Pit, let me show you my new “BitShifter” service! Here’s the module definition:
module BitShifter exposing
( BitShiftable, Operations
, shiftLeftBy, shiftRightBy, toBitShiftable
)
It lets you turn any type you like into a BitShiftable
, a value that you can then shift left and shift right, just like the bit shift methods in the Bitwise
core module.
The type BitShiftable
is opaque. You don’t need to know the exact implementation in order to use it. Such a value can be shifted bitwise with the functions
shiftLeftBy : Int -> BitShiftable -> BitShiftable
shiftRightBy : Int -> BitShiftable -> BitShiftable
Isn’t this great?
You just have to tell the service a little bit about your type. The Operations
record specifies what the service wants to know about your type in order to be able to to turn it into a BitShiftable
:
type alias Operations a =
{ up : Int -> a
, down : Int -> a
, value : Int
}
toBitShiftable : (a -> Operations a) -> a -> BitShiftable
So if you tell me how to create the up
, down
, and value
fields for any value of your type a
, plus a concrete a
value, I can turn the latter into a BitShiftable
!
Pit
Hey, this is nice, Rupert! But you can’t trick me The Operations
record told me your secret: you are using an interface behind the scenes, aren’t you?
Rupert
Hmm…
Pit
No problem. Let’s pretend I don’t know. I’ll test your service right away with my favorite type: a list of units…
Let me see… I’ll give you the operations and you turn my List ()
into a BitShiftable
…
He writes:
myBitShiftable : BitShifter.BitShiftable
myBitShiftable =
BitShifter.toBitShiftable
(\list ->
{ up = \n -> List.repeat n () ++ list
, down = \n -> List.drop n list
, value = List.length list
}
)
[ (), (), () ]
Pit
And now I can shift it left and right, right?
shifted : BitShifter.BitShiftable
shifted =
myBitShiftable
|> BitShifter.shiftLeftBy 2
|> BitShifter.shiftRightBy 1
Nice! But what can I do with the shifted result? Can I get it back as a List ()
to be able to look at it? What about a function like
fromBitShiftable : BitShiftable -> a
Rupert, who is a little disappointed about the course of the experiment, doesn’t want to give up yet, so he just says:
A function with this signature wouldn’t be possible, but let me think about it…
Of course he has been using the interface technology behind the scenes. Here’s his code:
type BitShiftable
= BitShiftable (Operations BitShiftable)
type alias Operations a =
{ up : Int -> a
, down : Int -> a
, value : Int
}
bitShiftableOps : (rep -> typ) -> Operations rep -> Operations typ
bitShiftableOps raise ops =
{ up = ops.up >> raise
, down = ops.down >> raise
, value = ops.value
}
toBitShiftable : (a -> Operations a) -> a -> BitShiftable
toBitShiftable impl =
IF.create BitShiftable bitShiftableOps impl
shiftLeftBy : Int -> BitShiftable -> BitShiftable
shiftLeftBy n (BitShiftable ops) =
ops.up (ops.value * (2 ^ max n 0) - ops.value)
shiftRightBy : Int -> BitShiftable -> BitShiftable
shiftRightBy n (BitShiftable ops) =
ops.down (ops.value - (ops.value // (2 ^ max n 0)))
Nothing special so far. He even used the same operations as in the Counter
interface to implement the bit shift operations. It’s no surprise that even Pit was able to see through the secret.
When he said that the fromBitShiftable
function from above wouldn’t be possible, he was right. What type should the function result be? It can’t simply be an a
like Pit suggested, because the a
could be anything.
But Rupert has an idea: what if the BitShiftable
type had a type parameter? A function like this would be possible to implement in Elm:
fromBitShiftable : BitShiftable a -> a
On the other hand, this would mean that the underlying type wouldn’t be hidden anymore, so it wouldn’t be possible to create a list of different BitShiftable
s anymore. For Rupert’s BitShifter
service, this wouldn’t be a problem. The goal of the service is to enable bit-shifting-like operations for a user-supplied type, not to make multiple different BitShiftable
values combinable.
Great. Rupert has a plan how to proceed.
So how could he implement the fromBitShiftable
function? He starts with a version that looks like the value
function from the Counter
interface:
fromBitShiftable : BitShiftable a -> a
fromBitShiftable (BitShiftable ops) =
ops.internal
OK. So he needs an Operations
record with an additional internal
field:
type alias Operations a =
{ up : Int -> a
, down : Int -> a
, value : Int
, internal : a
}
The new field has to be dealt with in the map
-like bitShiftableOps
function:
bitShiftableOps : (rep -> typ) -> Operations rep -> Operations typ
bitShiftableOps raise ops =
{ up = ops.up >> raise
, down = ops.down >> raise
, value = ops.value
, internal = ops.internal
}
Hmm. This doesn’t compile. To make the compiler happy, Rupert would have to raise
the internal value, too. But he doesn’t want to change the type of the internal
value.
Rupert knows how to deal with this problem: he has to introduce an additional type parameter to the Operations
record type, too:
type alias Operations i a =
{ up : Int -> a
, down : Int -> a
, value : Int
, internal : i
}
Now he can leave the internal
value as it is without having to apply the raise
function:
bitShiftableOps : (rep -> typ) -> Operations rep rep -> Operations rep typ
bitShiftableOps raise ops =
{ up = ops.up >> raise
, down = ops.down >> raise
, value = ops.value
, internal = ops.internal
}
This compiles! The BitShiftable
type now looks like this:
type BitShiftable a
= BitShiftable (Operations a (BitShiftable a))
Great! Now the toBitShiftable
function:
toBitShiftable : (a -> Operations a a) -> a -> BitShiftable a
toBitShiftable impl =
IF.create BitShiftable bitShiftableOps impl
Only the function signature had to be changed. Nice.
But Rupert knows: if I show Pit this new version, he’ll still be able to figure out what I’ve done just by looking at the types of the toBitShiftable
function.
Wouldn’t it be nice if Rupert could use the same Operations
record as before in his BitShifter
API?
Here’s how the user-supplied impl
parameter looked like in the old and in the new version:
old:
a ->
{ up : Int -> a
, down : Int -> a
, value : Int
}
new:
a ->
{ up : Int -> a
, down : Int -> a
, value : Int
, internal : a
}
Wait… Rupert is sure that he can turn the former into the latter!
He defines two different operations record types: one which is externally visible and an internal one:
type alias Operations a =
{ up : Int -> a
, down : Int -> a
, value : Int
}
type alias InternalOperations i a =
{ up : Int -> a
, down : Int -> a
, value : Int
, internal : i
}
Now he can convert the original impl
parameter with the Operations
record into a version using the new InternalOperations
record:
internalImpl : (a -> Operations a) -> a -> InternalOperations a a
internalImpl impl a =
{ up = (impl a).up
, down = (impl a).down
, value = (impl a).value
, internal = a
}
Using this helper function, we get almost the original version of the toBitShiftable
function:
toBitShiftable : (a -> Operations a) -> a -> BitShiftable a
toBitShiftable impl =
IF.create BitShiftable bitShiftableOps (internalImpl impl)
After all, this was easy!
Rupert shows Pit the new version of the BitShifter
module.
Rupert
Hi Pit. Do you remember the fromBitShiftable
function you wanted to have to be able to get back at the shifted value?
I told you that it wouldn’t be possible to implement it in the way you suggested. But if we add a type variable to the BitShiftable
type, then I can give you a function
fromBitShiftable : BitShiftable a -> a
Pit
Hmm, OK, so what would I have to change in my code?
Rupert
As I said: you have to add a type parameter.
Pit
Wait. That’s all I need to do? Everything else stays the same? If this is true, then you’re slowly but surely turning into a type system wizard like Jeremy!
Let me try it…
He adds the type parameter to his code:
myBitShiftable : BitShifter.BitShiftable (List ())
myBitShiftable =
BitShifter.toBitShiftable
(\list ->
{ up = \n -> List.repeat n () ++ list
, down = \n -> List.drop n list
, value = List.length list
}
)
[ (), (), () ]
shifted : BitShifter.BitShiftable (List ())
shifted =
myBitShiftable
|> BitShifter.shiftLeftBy 2
|> BitShifter.shiftRightBy 1
Pit
And now I can get back the original and the bit-shifted value?
He types:
myBitShiftable
|> BitShifter.fromBitShiftable
--> [(), (), ()]
shifted
|> BitShifter.fromBitShiftable
--> [(), (), (), (), (), ()]
Pit
Jeremy, um, I mean, Rupert, you are a genius!
I can’t wait until you explain your wizardry to me!
Rupert smiles. Mission completed.
-- ::: --
Ok, this was the last aspect of the interface technology I wanted to look at. I found it interesting that you can hide a set of user-supplied functions behind an easy-to-use interface type, and that you can hide them from both the users of a module as well as from the module code itself.
In the BitShifter
example, both the module user as well as other module functions can use this simple interface:
type BitShiftable a
shiftLeftBy : Int -> BitShiftable a -> BitShiftable a
shiftRightBy : Int -> BitShiftable a -> BitShiftable a
In the next days, I’ll add a short recap with the three versions of Jeremy’s magic interface functions we’ve seen before, … and then one last thing.