When we think about interfaces in software development, it’s easy that our thoughts go the interface keyword used in many Object-Oriented languages like Java and C#.
But interfaces are a much more general concept, and they could be defined as:
an interface is a shared boundary across which two or more separate components of a computer system exchange information
- Wikipedia
Even restricting to the realm of software architecture, an interface is an abstract specification which allows declaring a shared boundary between one (or more) consumer and one (or more) producer. This allows consumers to not know any details about the producers and, likewise, producers not to know details about the consumers. Hence, interfaces can help us decoupling software components that need to collaborate, keeping our application modular.
In particular, there’s nothing specific about Object-Oriented programming when it comes to interfaces. In fact there’s even purely functional languages which are using an interface keyword.
In this post, I’d like to show and discuss four different ways for declaring interfaces in Haskell.
The consequences of an interface
To compare multiple ways of declaring an interface, we should consider all the aspects involved with the declaration and the usage of an interface.
For the sake of the example, we will abstract the following operations through an interface in four different ways.
where DB has a monad instance.
Please remember that an interface is used to decouple two systems, and it should be the one defining the desired API, not indulging in technical details, while the specific implementations are the ones which should adapt to the interface definition.
The first aspect we should consider is the definition of the interface itself. How do we say that such an interface exists? And how do we declare its API?
The second aspect is how we can define implementations for a given interface. How easy is it to do so? How clear is it that what we’re defining is an instance for a given interface?
Then we need to consider how we can consume an interface. How easy is it to work with an abstraction defined in such a way?
The last aspect to consider is how the wiring happens. How can we say that a specific implementation should be used?
In the next section we’re going through four different ways of defining an interface in Haskell, trying to describe how each one of these aspects work.
Interface as a typeclass
The most common way to declare an interface in Haskell is probably by using a typeclass. The declaration of the interface looks like
An implementation of this interface is defined by providing an instance for the typeclass for a concrete type
where we are using the functions defined in the previous section.
Using the interface is a matter of writing code which has a UserRepositoryClass constraint. For example, we can define a function to update a User like
Connecting the interface with a concrete implementation is a matter of specifying the type which we want to use
I think this style is very well known and widely used. Let’s now proceed to other, probably less used, approaches.
Interface as a record
Typeclasses are very powerful and allow hiding a lot of boilerplate by resolving constraints behind the scenes. There could be cases, though, where one desires to have more control, or simply a more explicit management of the application components.
To achieve this one could decide to renounce to the magic given by typeclasses and use a basic record of functions as an interface.
This is really similar to the definition using a typeclass, but it uses data instead of class. It still abstracts on the context m, allowing the same level of decoupling one could have with the typeclass style.
Defining a concrete instance is a matter of providing a concrete value for the above defined type
Using the interface declaration means accepting as an argument a value of the above defined type
And selecting a concrete implementation means passing the desired value as an argument
As mentioned earlier, this style allows for a more explicit management of the concrete implementations. This could be especially useful if you need to access and interact with such values at runtime.1
Interface as a free monad
Another fairly common approach is using a free monad. This means expressing our interface as a sum type instead of a product type
Every constructor represents a potential action that we might want to do. What was the return type of the various actions in the previous approaches, here becomes the argument of a continuation.
Defining a concrete implementation means interpreting our constructors in a concrete monad
Using an interface defined through such an approach means building a value combining the above defined constructors. The implementation of the same functionality defined for the other two approaches would look like this
Connection the usage of the interface with a concrete implementation then looks like
The benefit of using a free monad approach is that you’re just building values with could be later interpreted. In the meanwhile, you can manipulate this values and potentially perform some static analysis on them.
On the other hand, for example, using two interfaces defined with free monads in the same functionality, proves to be more complex than what it is using two interfaces defined with one of the other approaches described above.
Interface as a GADT
A variation of the free monad approach could be defined using a GADT instead of a simple sum type using continuations
As you can see, with respect to free monads, here constructors hold functions and not product types, and we don’t have to use continuations.
Similarly to the free monads approach, a concrete implementation is an interpreter in a concrete monad
To use the interface we need to pass as an argument a potential interpreter
Connecting the usage to a concrete implementation becomes a matter of passing a function argument
With respect to free monads this approach is easier to use and does not require understanding Free, but it forces our domain code to depend on an interpreter (the f in the previous snippet. It’s not a concrete interpreter, so the decoupling is still happening).
Conclusion
This post presents four techniques for declaring interfaces in Haskell and briefly discusses and compares them. I’m fairly sure that there are other way people use to declare abstractions and decouple them from their implementations. I’d be quite curious to learn about them, so please feel free to contact me to share with me (and possibly the rest of the community) other ways to accomplish that.