Here is the next one: Separating Interface and Implementation
Some of us will have first encountered this in C, with its â.hâ header files defining data types and function call signatures, and the â.câ file with the implementing code in it. As a student I was made to learn Modula 3, which was really quite a pleasant language to use. It made this separation of interface and implementation more formal - every module had to have a corresponding interface. We did a group project where each member of the group first worked out what their interface was going to be, then everyone implemented their code using those interfaces, then we put the whole lot together and it mostly worked. Some time before that I got into C++, and it added classes to the .h files. You could spec out a class design for some particular behaviour you were trying to model, but that would permit alternative implementations of the details that your spec did not constrain. It was exciting to program in such an open ended way, but also sometimes distracting with all those clever patterns.
I tend to use âInterfacesâ quite a lot in Java code. For example, I wrote a network stack processor, and separated the network, codec, and session handling layers with interfaces. This made testing a lot simpler, as each layer could be plugged into a test harness which provided an alternative implementation of the interfaces the layer needed. At one point we even had an interface writing to a unix socket, that had a simulator running a hardware design for an off-loaded codec on the other end, and plugged the software session layer into that - made possible because of the use of interfaces up-front in the original software design.
Right now, I have a team developing software in the office where I work, and another team in another office in another country. The work has been divided up along the lines of: data analytics in this office where the subject domain experts are, cloud infrastructure for the analytics in the other office - also our lone wolf UI developer (he uses Angular not Elm, but he can also crank out UIs impressively fast). They need interfaces so they can agree on things up-front and then build those things, and feel fairly confident that they can bring their pieces together and make everything work. So interfaces in software are important in helping things scale, and they are needed in application code, not just in library code.
So where are the interfaces in Elm? First lets try a fairly direct translation of an interface in Java into Elm:
/** I need to put many data elements somewhere and get them back later, possibly in a different order. */
interface Buffer <E> {
boolean add(E e); // throws IllegalStateException if item cannot be added.
E remove(); // throws NoSuchElementException which is a runtime exception.
E element(); // throws NoSuchElementException which is a runtime exception.
}
type alias Buffer e buffer =
{ add : e -> buffer -> buffer
, remove : buffer -> Maybe ( e, buffer )
, element : buffer -> Maybe e
}
Java Buffer also has poll() and peek() methods but these use nulls when the buffer is empty. We donât silently fail in Elm so I took the versions which throw exceptions and translated those into Maybes.
Something interesting happened, the Java version has only one type variable E
, but the Elm version has two, e
and buffer
. The type variable buffer
had to be added to provide somewhere to put the implementation.
Here are some alternative implementations of Buffer:
stack : Buffer e (List e)
stack =
{ add = \item list -> item :: list
, remove =
\list ->
case list of
[] ->
Nothing
x :: xs ->
Just ( x, xs )
, element = List.head
}
queue : Buffer e (Array e)
queue =
{ add = Array.push
, remove =
\array ->
Array.get 0 array
|> Maybe.andThen
(\result ->
Just
( result
, Array.slice 1 (Array.length array) array
)
)
, element = Array.get 0
}
With reference to the thread on Dethroning the List, you can see that a Collection e collection
type alias could be defined. This could have List and Array implementations with different performance characteristics - code could be written to work with either by working through the Collection e collection
interface. (Not that I am saying this should be done - something more built into the language will likely be more performant).
âaddâ, âremoveâ and âelementâ are named functions. Together as the Buffer record type they form a set of functions for working with buffers. Higher order functions that can do stuff with buffers can then be written. When only 1 function is needed, it is usually passed as an anonymous lambda, such as mapping over Lists:
map : (a -> b) -> List a -> List b
The interface there is (a -> b)
, the most general one. So compared with Java, Elm is very nice in that it lets you deconstruct interfaces down to anonymous function types if you like, or to create records with named functions for larger structures.
In order to keep the interface and its implementation separate, the consumer of a module of code must be able to instantiate the implementation, but other than that should just depend on the interface. The implementation needs to depend on the interface too, in order to know its type. So you end up with a dependency graph something like this:
The implementation of an interface must be able to be instantiated at runtime and multiple implementations must be possible. Understood that way, are Elms modules interfaces? No, because the interface as type annotations in a module must be placed next to the implementation. Also modules are not instantiated dynamically.
Note that a stack and a queue do not have the same type, although both unify with the Buffer type; I cannot have a list with both stacks and queues in it. But it is possible to write functions over the Buffer type that will work with both the stack and queue implementations. This is a difference with how Interfaces in Java work, as an Interface in Java sits at the top of a hierarchy of sub-types. Library code can be open-ended but applications must eventually declare the actual types and implementations.
It is not uncommon in Java to see downcasting to implementations or making use of the instanceof
operator - and this is more strictly dealt with in Elm by the use of tagged union types and case statements.
A final difference to note is that in my queue.remove
operation above I wrote Array.get 0 array
rather than just call this.element
which already had that as its implementation - this is not actually OO programming.
Interfaces can be used where:
- Larger project needs some formal interfaces to separate areas of concern between programming teams.
- To guide the larger structure of a program, even where work is not split between teams or even individuals. There can be a benefit to being able to compile structure before writing an implementation.
- To plug-in variable functionality by substituting implementations where high flexibility yields a benefit.
- To code in an open-ended way (dangerous).
- In every day coding whenever we use higher order functions.