I am currently employed as a Java developer, but I’ve only held this position for a short while. Before that, my official job title was “data engineer” which made me want to throw up because:
But that’s besides the point.
The point is that, even though I am a professional java developer, I don’t know that much about it. I care much more about the niceties and warts of my favorite hobby languages than about the one I use nearly every day for my employer.
That being said, I came across a facet of java today which I had completely forgotten about. One which I am sure there is a name for. The thing in question was a function return type of an interface.
Now, being a Haskellfag this immediately struck me as odd
and I won’t lie that it got on my nerves. I know that it’s ignorant to
say that typeclasses are basically interfaces but this is the space that
they occupied in my mind. I had totally forgotten how interfaces could
work in practice.
One way I think of typeclasses and interfaces is that, in isolation, they hide information about their implementing type and expose only a subset of the total operations that may be available to use on that particular implementing type.
class Iterable t where
next :: t a -> Maybe (a, t a)
Here is a typeclass which generalizes the type signature laid forth
by Data.List.uncons :: [a] -> Maybe (a, [a])
. By itself,
an Iterable
constraint tells you nothing other than the
fact that you can pop an element off of a non-empty structure.
For example:
tail' :: Iterable t => t a -> Maybe (t a)
= fmap snd . next tail'
The above function can take the tail of any iterable structure, not
just a list. With the caveat that the function itself doesn’t know
anything about its input argument other than that it has an
implementation for next
.
So now consider a function with this signature:
generalizeIterable :: Iterable t => [a] -> t a
It should be immediately obvious (to anyone familiar with Haskell) that, in this context, this function has no possible implementation.
The reason for this is because the type variable t
must
polymorphically represent any type which has an Iterable
instance. t
cannot be bound to a concrete type, but you
can’t return anything but a value of a concrete type from a function.
Even if this function were to return a [a]
or a
Maybe a
, this is a type error of this flavor:
• Couldn't match type ‘t’ with ‘[]’
Expected: t a
Actual: [a]
‘t’ is a rigid type variable bound by
the type signature for:
generalizeIterable :: forall (t :: * -> *) a.
Iterable t =>
[a] -> t a
That this is a type error trains our intuition about how Haskell programs generally work, type-wise. They flow from general types to specific types, or polymorphic functions to concrete types.
Consider now the rough Java analogue:
public interface Iterable<T> {
public Optional<Pair<T, Iterable<T>>> next(Iterable<T>);
}
public class IterableList<T> implements Iterable<T> {...}
public Iterable<T> generalizeIterable(IterableList<T> xs) {
return xs
}
Excuse any mistakes in this code, I really don’t care to actually try to compile this snippet to see if its valid.
But notice how generalizeIterable
is perfectly
acceptable!
Consider that the Java implementation has a Haskell analogue via existential types.
data IterableProxy a where
IterableProxy :: Iterable t => t a -> IterableProxy a
generalizeIterable :: [a] -> IterableProxy a
= IterableProxy generalizeIterable
So we can hide the Iterable
constraint inside the
constructor of an IterableProxy
and then turn a concrete
type, []
, into a less concrete type
IterableProxy a
. This type is less concrete because we lose
access to the information that the Iterable
instance is a
[]
and therefore we lose access to all of the functions
from Data.List
, keeping just next
that’s
declared in Iterable
.
You wouldn’t. I was going to say something like
Java generally has expansive enough interfaces to be useful in cases where
an interface constraint is the only information you have. Haskell type classes are generally
more terse and are therefore strictly less useful in more cases than concrete types are.
but after one half-second of thought I realize this is nonsense. I’ll double down on my opinion that this pattern of returning an interface rather than a concrete type in Java is pukeworthy and I sincerely implore Java developers to repent from their ways and learn Haskell.
I think, on a semantic level, hiding type information in the return type of a function just obfuscates the meaning of code and even further confuses implementation. Why, as the author of such code, do you return an interface and not a concrete implementation of an interface? Your type signature is telling me that you yourself do not know what your function is producing! Why would you not declare a new class, with a meaningful name, implement your interface, and enrich the informational landscape of your program rather than obfuscate it?
To me, this a design decision that should live in the internals of a library but in this case it leaked out and caused me immeasurable emotional anguish.
Thank you for reading.