Skip to content

Instantly share code, notes, and snippets.

What would you like to do?


I was recently asked to explain why I felt disappointed by Haskell, as a language. And, well. Crucified for crucified, I might as well criticise Haskell publicly.

First though, I need to make it explicit that I claim no particular skill with the language - I will in fact vehemently (and convincingly!) argue that I'm a terrible Haskell programmer. And what I'm about to explain is not meant as The Truth, but my current understanding, potentially flawed, incomplete, or flat out incorrect. I welcome any attempt at proving me wrong, because when I dislike something that so many clever people worship, it's usually because I missed an important detail.

Another important point is that this is not meant to convey the idea that Haskell is a bad language. I do feel, however, that the vocal, and sometimes aggressive, reverence in which it's held might lead people to have unreasonable expectations. It certainly was my case, and the reason I'm writing this.

Type classes

I love the concept of type classes. I gave talks about type classes on multiple occasions, and encourage everyone I work with to use them more. But I don't like Haskell's.

The main reason for that dislike is simple: the community, language, toolchain... all try to convince you that orphan instances are evil. The one person who doesn't seem to hate them is Simon Peyton Jones, magnificent man that he is.

Superfluous dependencies

Imagine a project which must process documents of some kind. It could, say, count the number of words in these documents. The actual processing is irrelevant to my point.

This project must have two user interfaces: a CLI, and a web API.

I tend to structure such projects intuitively:

  • one core module with all the business types and functions
  • one cli executable for the CLI specific part, with a dependency on core
  • one api executable for the web API, with a dependency on core

The problem arises when I decide to represent Document as JSON. I need to provide the correct aeson instances for Document - but these would be orphaned instances, and orphaned instances are bad.

What are my options, then?

  1. newtype Document in the api module - boilerplate galore for something that is arguably just a matter of principle
  2. declare the instances in the core module - bringing the entirely unrelated aeson dependency in the cli module

This is not a huge problem, but it's frustrating.

Lies and damned lies

One of the main arguments for type classes, and their superiority to inheritance, is that you can add new behaviours to data types you don't own. But isn't that a bit of a lie, if you disallow orphaned instances?

You can add an instance of a type class you own to a type you don't, or of a type class you don't own to a type you do. And that's it. A truer argument would be that you can add behaviours to types you only partially own, but that sounds far less exciting.

The thing is though, given this constraint, type classes are not much better than nominal subtyping (when it comes to adding behaviours to types). If I can't modify Document and want to give it an instance of, say, Show, I must create a new type that wraps a value of type Document (albeit at no runtime cost). Just like you would if you wanted to make an existing type Comparable - create a wrapper that extends Comparable.

And, of course, when you newtype Document, you lose of all its other instances. And yes, I'm aware that Haskell has very clever, very well designed mechanisms to re-create these instances at very little human cost - but then, what's to prevent Java 28 from defining an annotation of some sort to automatically extend the wrapped type's superclasses, and proxy all calls to the wrapped value? Wouldn't that make inheritance just as flexible as Haskell's type classes?

And yes, I'm aware that type classes have other advantages - implicit composition, for instance. But I can't help but feel that the usual Type classes allow you to add behaviours to types you don't own spiel is a bit of a fib, if you're not going to allow orphan instances.

Global coherence

Yes, I understand that global coherence is nice. Yes, orphaned instances make it much harder to guarantee global coherence.

But I feel local coherence is not worse than global coherence, provided it's well enforced.

The usual example against locally coherent type class instances is sets implemented as binary sorted trees. How do you merge two sets when you can't know if they're using the same notion of ordering? Global coherence sorts that out - you can't possibly have different notions, because you can't possibly have more than one type class intance for that type in the entire system.

It seems to me that, in some languages (by which I mean Scala), that notion of ordering is materialised as a value. And, in languages supporting the popular definition of dependent types (by which I mean, not Scala), you could bring that value to the type level - naively, and without any real skill in that domain, I feel that if you can have the compiler refuse to zip two lists with different lengths, you should be able to have it refuse two sets with different orderings.

I don't know of a language that does that. Maybe it doesn't exist. Maybe it's a terrible idea. Maybe there are very good reasons for this to be silly. I just haven't found one yet.

Runtime type errors

Haskell has exceptions. Unchecked exceptions. And people feel it's a good thing. I don't even.

One of the main Haskell selling points (and rightly so!) is that its powerful type system allows you to write code that, if it compiles, works. And yet, unchecked exceptions make it impossible for the compiler to guarantee that you've dealt with all possible error cases - it'll cheerfully accept code that will crash, and let you find out about your mistakes at runtime. That's exactly what I'd like a type system to not do.

I honestly don't understand how you can both state that Haskell code, when it compiles, just works and Unchecked decisions are a very good point in the error handling design space. The two seem at odds to me.

I've tried to read arguments justifying the existence of exceptions in Haskell. I've asked people that are actually proficient with the language. All I've got so far is it's no worse than Java! which, granted, but that's not exactly how Haskell is sold, is it?

As a small aside, I'm perfectly happy with exceptions for exceptional circumstances - cpu not found, oom... On the other hand, Invalid credentials, connection loss, invalid request... are not exceptions. Those are regular, normal circumstances that must be dealt with before a program is considered complete. And yet a lot of libraries I've tried deal with them as exceptions, expecting me to somehow know all the error case I must deal with and how they've been modeled.


Ah, yes. This is what will get people really riled up. I think Haskell's fetishism of monads is unhealthy. I don't like monads. The fact that different monads don't compose unless you know exactly what monads they are and have written or obtained the glue code is, to me, proof that we have better abstractions to discover.

I know I'll get shot for this, but I think mtl (what little I understand of it) is bad. Well. It's a really good implementation of a bad solution. The fact that it's essentially boilerplate central, writing all that mind numbing code for all possible combinations of monads so that users don't have to, is to me a very strong hint that something's not quite right.

See for yourself:

instance MonadError e m => MonadError e (IdentityT m) where
    throwError = lift . throwError
    catchError = Identity.liftCatch catchError

instance MonadError e m => MonadError e (ListT m) where
    throwError = lift . throwError
    catchError = List.liftCatch catchError

instance MonadError e m => MonadError e (MaybeT m) where
    throwError = lift . throwError
    catchError = Maybe.liftCatch catchError

instance MonadError e m => MonadError e (ReaderT r m) where
    throwError = lift . throwError
    catchError = Reader.liftCatch catchError

instance (Monoid w, MonadError e m) => MonadError e (LazyRWS.RWST r w s m) where
    throwError = lift . throwError
    catchError = LazyRWS.liftCatch catchError

instance (Monoid w, MonadError e m) => MonadError e (StrictRWS.RWST r w s m) where
    throwError = lift . throwError
    catchError = StrictRWS.liftCatch catchError

instance MonadError e m => MonadError e (LazyState.StateT s m) where
    throwError = lift . throwError
    catchError = LazyState.liftCatch catchError

instance MonadError e m => MonadError e (StrictState.StateT s m) where
    throwError = lift . throwError
    catchError = StrictState.liftCatch catchError

instance (Monoid w, MonadError e m) => MonadError e (LazyWriter.WriterT w m) where
    throwError = lift . throwError
    catchError = LazyWriter.liftCatch catchError

instance (Monoid w, MonadError e m) => MonadError e (StrictWriter.WriterT w m) where
    throwError = lift . throwError
    catchError = StrictWriter.liftCatch catchError

If a Java library were to copy / paste quite that much code, just changing the types and a few different details here and there, it'd be mocked relentlessly. But since it's Haskell, and those are monads, then clearly it must be good. To me, however, this looks a lot like the 27 implementations golang of a linked list I had to write because of lack of parametric polymorphism. There might be a subtle distinction, I've just not yet seen it.

What little I understand of algebraic effects make me feel that they're a much better solution to most (all?) the problems that monads try to solve. And yes, they're hard to add support for, but clearly impossible - it has been done.

Copy link

rpeszek commented Feb 20, 2021

I am clearly late to comment on this. I think this is somewhat different from other comments so I will still post it.
FYI, I am using Haskell professionally. I have used Java (you have mentioned it) professionally as well,

The main question that pops to my mind is this: So which language is better? I have not found one that is ready for industrial use.

Runtime Errors: The argument can be made that there are too many libraries on Hackage that abuse non-termination.
Good example is the dreaded decodeUtf8 which, IMO, just should not be there.
I would not criticize the language for this. To avoid untyped errors you would need dependent types or something equally powerful. Consider a / b computation where a and b are ints. Do you want a checked exception on division?
You can argue that Haskell's IO monad should explicitly type exceptions like IOError and I agree that it would be nice to see IOError's and such in type signatures. This is not how Haskell or similar languages have been designed.

Orphan Instances: People do use them. newtype does not cause "boilerplate galore", there is -XGeneralizedNewtypeDeriving which will create instances for you. In fact, automatic boiler plate code creation is where Haskell is simply second to none. You typically get these for free: Eq, Show, Functor, Functor, Foldable, Traversable, ToJSON, FromJSON and more.

Monads simply are. Criticizing the concept is like criticizing multiplication. It is a fair game, IMO, to criticize application of mathematical abstractions to certain types of computing, not the abstractions themselves. Such critique would be hard to do with Monads, they are really a very good polymorphic abstraction over a lot of different types of computations.

About transformers vs extensible effects (e.g. polysemy), this is not one vs the other. They work well in tandem.
This aside, learning transformers is really a great way to expand understanding of computations. You learn how computations compose as types. E.g. how (a -> _) composes with (a,_) and you start to see the properties of such compositions.
You can think about a monad as a mini-embedded language with protection of his boundaries. transformers explicitly compose such embedded mini-languages.

Since you have mentioned Java, I would challenge you to look at vavr which has to deal with a lot of duplicate code because it cannot express certain polymorphic expressions which are trivially implemented using the Monad (mapM, sequence, etc) and has to re-implement them for every type. About the code duplication example you have shown: try to do that in Java!
You see Java (and golang) have you stuck with: SomeType <T>, what you need it T R where T is a type variable parameterized by R, very few languages can do that!
With that in mind, notice Functor in the above Haskell free boilerplate list. Functor is now a familiar (even if very underused) thing in mainstream languages. Mainstream langs (like Java) would have to implement map separately on each type, separately for list, map, option, binary tree, rose tree, function, tuple, ... there are very many functors and the lang typically settles on just one (list) or a very few.
You also see equals boilerplate, toString boilerplate in mainstream language code.

It is also good to try to understand what the code you presented does, e.g.

catchError = LazyRWS.liftCatch catchError

mtl provide polymorphic versions of transformers , this code simply delegates to transformers non-polymorphic liftCatch. Delegating to other implementation is what you see in all languages. The technique is familiar, but what this code actually achieves is mind-blowing if you come from a different language.

Coherence Haskell is, I believe, unique in having this property.
Edward Kmett's presentation about it:

With all of this said, I think this was a well written piece. I would like to commend you for writing it.
I hope my comment is helpful in filling out some blanks which make Haskell look bad if they are left bank and make Haskell look quite good if you fill them up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment