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 BitShiftables 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.