OO-style interfaces for Haskell
Haskell doesn’t have interfaces which are a fundamental mechanism to achieve modularity in object-oriented languages. This might seem strange at first, but it turns out that isn’t a problem because type classes fill a similar role. In this post, however, I’ll present a more direct way that can be used to simulate interfaces using them.
You might be able to deduce something about the expressivity of one mechanism compared to the other, but I’m not nearly informed enough to do that. Besides, I’m sure it’s already been done. This is mostly meant as an interesting proof of concept. Not an actual, useful thing.
The class
The actual machinery to do this is laughably short. So short, in fact, that I’ll couple it with an extra operator we’ll use for convenience later.
class Interface c i where
i :: c -> i
infixl 1 -->
(-->) :: a -> (a -> b) -> b
(-->) a b = b a
At the type level, the class represents pairs (c, i)
of types c
that implement the interface i
.
At the value level it provides a method to lift an object of type c
so it can be used through one of
the interfaces it’s type implements.
IEnumerable
The example I’m choosing to use is the IEnumerable
interface from .NET. It’s pretty much strictly
worse than the existing type classes in Haskell that do the same thing, but it’s not like I’m
trying to make some point here. The interface itself is interesting enough to be a good candidate
for an example.
We’re actually going to need two interfaces here. The IEnumerator
and the IEnumerable
.
The IEnumerable
interface declares only one method: getEnumerator
which returns an enumerator
of the same generic type. This will all be clear in the signatures.
The IEnumerator
is slightly more involved and declares 3 methods and one property.
The property will just be a “parameterless” method. One of the methods is Dispose
which we’ll ignore.
I’ll also take some liberties with the signatures of the rest to make them make more sense in Haskell.
The main idea behind an interface will be a structure that the implementer is required to fill.
So here are their definitions.
data IEnumerator t = IEnumerator
{ current :: t
, moveNext :: Maybe (IEnumerator t)
, reset :: IEnumerator t
} deriving Show
data IEnumerable t = IEnumerable
{ getEnumerator :: IEnumerator t
} deriving Show
The moveNext
traditionally mutates the current enumerator, but I decided to maybe return a new one.
The Maybe
is there because the method should fail if we’re at the end of the collection.
reset
returns the enumerator to the beginning and current
is self explanitory.
Making a list enumerable
The only type I’ll implement the interface for is the basic list.
To that end we declare a new type, ListEnumerator
that will be our IEnumerator
.
data ListEnumerator t = ListEnumerator
{ leCurrent :: [t]
, leStart :: [t]
} deriving Show
leMoveNext :: ListEnumerator t -> Maybe (ListEnumerator t)
leMoveNext le = case leCurrent le of
(_ : x : xs) -> Just $ le { leCurrent = x : xs }
_ -> Nothing
leReset :: ListEnumerator t -> ListEnumerator t
leReset le = le { leCurrent = leStart le }
And finally, we make the ListEnumerator
implement IEnumerator
and the list itself implement
IEnumerable
.
instance a ~ b => Interface (ListEnumerator a) (IEnumerator b) where
i le = IEnumerator
{ current = head . leCurrent $ le
, moveNext = fmap i . leMoveNext $ le
, reset = i . leReset $ le
}
instance a ~ b => Interface [a] (IEnumerable b) where
i l = IEnumerable . i $ ListEnumerator l l
There’s one interesting thing here, and that’s the equality constraint (~).
My first approach was instance Interface (ListEnumerator a) (IEnumerator a)
and this works, but
using it is problematic. Ideally, we can’t expect i x
to return anything sensible because we have
no idea which interface we want to lift the object x
to. But what we would expect is that something
like getEnumerator $ i x
does resolve. Unfortunately, with the above setup it wouldn’t.
The reason is that, while x :: [a]
might implement the interface IEnumerable a
, it could potentially
also implement IEnumerable b
for some other type b
. Because of this, getEnumerator $ i x
is still
too ambiguous of an expression to do something with it because the compiler can’t decide which
IEnumerable
we want.
The fix (and I was pretty surprised it works that well) is in the equality constraint.
If you look at the head of the instance (which is the only thing that matters when the compiler
checks for instances), we have Instance [a] (IEnumerable b)
. This doesn’t say anything about the
types a
and b
so it matches all of them. This lets the compiler know that there’s only one
instance that could be used here.
Next, the a ~ b
part lets it infer one type from the other, as long as one of the is known.
Usage
.NET defines a ton of extension methods for the IEnumerable
interface. Extension methods in Haskell
don’t really differ from normal functions though.
I implemented some of them so there’s something to play with. I won’t go over them in this post,
but feel free to check out the code.
Here’s an example use case of our interface using those extension methods
i ["hello", "world", "how", "are", "you"] --> where' (\s -> length s > 3)
--> select (\s i -> s ++ "!")
--> toList
> ["hello!", "world!"]
Neat!
The error messages are also pretty good.
i 'a' --> where' (\s -> length s > 3)
--> select (\s i -> s ++ "!")
--> toList
No instance for (Interface Char (IEnumerable [Char]))
Summary
This was certainly a fun venture, though I can’t speak for the practicality of it.
One use case that comes to mind are functions like C y => x -> y
where we would like to allow
functions to only be able to return some y
of the class C
. Not ANY y
. To this end we could use
the described method in a relatively painless way.
The code as it looks at the time of the writing is available here.