Why do you prefer Clojure over Haskell?

Some background: I worked professionally in Haskell for over two years from 2010-2013. I have been programming in Clojure since 2008.

I’ve thought long and hard about why I prefer Clojure to Haskell, even though I think Haskell is a very fine, well-designed language. I’ve tried to figure out the precise points, which have evolved over the years. My Haskell experience is only a little in a college class and at one company, so I’ve tried to separate the company’s practices from the language (which was hard!).

Every language has faults, and I’m not just going to pile onto the problems with Haskell. I want to focus on the few things that matter in my choice.

Okay, so here are the reasons why I prefer Clojure:

  1. Fast feedback is the number one thing I look for in a language.

    I found in Haskell that it might take 2 hours to get code to compile. People would say that “once you get it to compile, it just works”. But I found that I often couldn’t even remember what I was trying to do. Or I would run it and realize it wasn’t doing what I wanted and it had to be redone. 2 hours just to find that out!

    Though very often the compiler errors were useful and helped guide me, I was totally divorced from the actual problem I was solving for that long period of time. That never happens in Clojure. There are long debugging sessions in Clojure, but I’m in there with the code and the problem.

    Even without that problem, Clojure just wins here. It is much easier to write code at the REPL and eventually get it into production code.

  2. Library ecosystem

    Back in 2012, we did lack some libraries. We were shelling out to curl for some things because the libCurl wrappers didn’t have the feature set we needed. It worked, but it was less than ideal. There was nothing we needed to do that we couldn’t work around somehow. This has likely changed in the years since I’ve worked in Haskell.

    Clojure gets to use the industry tested Java libraries. Clojure wins here. I’m also familiar with the JVM, so I have a personal advantage there.

  3. Haskeller in Clojure land

    I’m not an expert Haskeller by any means, but I did learn a lot about its powerful type system. I eventually learned to write code that would compile the first time–which meant having a copy of the type checker running in my head. Now, when I’m in Clojure, I can still use that type checker. I can take it with me wherever I go. It’s not quite the same as having it enforced automatically, but it’s there.

    Now, what that means is I can use the good parts of Clojure and the killer advantage Haskell has. If the type checker or the strict lack of side effects is getting in the way, I can still use the flexible Clojure language. But if I want the strict type discipline, I’ve got it.

    You can’t get that the other way–a Clojurist moving to Haskell. This is of course based on personal experience, so I can’t expect others to have this advantage. And this doesn’t work on a team, either, where Haskell would enforce the types regardless of good or bad practices.

All that said, I often miss the type system. And I want to check out Haskell again soon!

8 Likes

I think function composition and currying, when used extensively, require static typing. They don’t provide much visual semantic information to indicate errors. This is something Gilad Bracha has commented on. Point-free style is so error-prone because of it that you need someone to check your back. And it seems like you’re expressing that same phenomenon in a different way.

That said, I rarely use function composition and currying in Clojure. If you want to do a lot of composition and currying, my suggestion is to use Haskell.

I realized this morning that no one seems to have responded to this question! Yes, we tried core.typed at work. I can’t remember whether we tried Schema first, but we went back and forth several times between “plain Clojure”, Schema, and core.typed. Then we went back to plain Clojure for a while and now we’re using Spec.

On the plus side, core.typed let us “type check” our code separately from runtime. On the negative side, idiomatic Clojure code often contains constructs that are extremely hard to describe in core.typed and we found ourselves either telling core.typed to ignore certain functions or rewriting the code to less idiomatic constructs purely to satisfy core.typed. Also on the negative side, once you started annotating code with core.typed, it required whole namespace annotation (even if it was just to tell core.typed “Don’t check this function yet!”). It also tended to bleed into other namespaces (because core.typed needed to know about the functions you call), and that often bled into the third party libraries you used (we started writing annotations for clojure.java.jdbc, for example, because we used it so heavily).

So we added core.typed annotations and then removed them (several times) and we added Schema annotations and also removed those (several times). Schema provides rich runtime checking but it has most of the syntactic “noise” of core.typed and your type checks are only as good as your unit tests – beyond that, you’re just doing runtime checking with more detail.

Why are we using Spec then? It feels like less noise than Schema, it has Clojure/core support (which also means it pulls in fewer dependencies), and the integration with test.check and the whole data generator side of things has been very valuable for testing code.

I think core.typed is great if you buy into it wholesale from the first line of code in your project (and you’re not relying on too many third-party libraries). It would be much more useful if it had better type inference and also treated unannotated functions as unchecked (so you don’t have to annotate as much code) – in other words, it needs better support for the boundary between checked and unchecked code, which Ambrose has talked about, as have others in this space (look at Typed Racket and recent work there for example).

4 Likes

My PurefunctionPipeline&Dataflow testing and debugging is also this way, just using ; instead of (comment ) , because editor single-line comment are more convenient, regular expression lookup (defn test-fnxx []) or ; (defn test-fnxx []) is very easy.

functions and its test (debug) functions put together for easy debugging, reading and synchronizing updates.

and an ->> thread macro observation function: #(do (prn %) %)

The slack group just called Functional Programming would be a good start, they have a very active Haskell channel with a several prominent community members (thinking Edward Kmett and Chris Allen).

I found this to be one of the benefits of programming in Miranda at school (plus Haskell and OCaml in industry). Ultimately, I prefer to work in scheme-like languages, but adding another set of cognitive tools often provides useful carryover. Similar benefits in other directions can be derived from asm, APL, FORTH and Smalltalk.

1 Like

For beginners, there’s the #haskell-beginners channel over there.

Is it that an interactive style of development was not a common practice? A few months ago I was writing a small Haskell project with Stack. I used Emacs, Intero and a GHCI frame and it seemed to map well to the workflow I use with Clojure and CIDER.

Have you seen Eta?

Well, things have probably progressed quite a lot since I worked full time in it. For instance, we didn’t use Stack. I’m not sure if it existed.

I think the interactive programming is hindered by the type system, though there are probably workarounds. For example, let’s say you have a type called A and a function called f, that takes an A as an argument. They’re used quite a lot throughout the program. How do you interactively modify A and f to implement some new functionality? Once you change either, you start getting errors. This may be different, now, but at the time, I faced this constantly.

The best advice I’ve gotten is to make a new type A2 and a new function f2, which start off as copies, and so everything still compiles. You can then modify them, since they’re not interwoven throughout the code. Once you have them modified the way you want, you can use the same technique to make a g2 and h2 (modified copies of g and h, which call f). You wind up with about twice as much of everything, but you always have working software. Then you can go back and delete all of original functions. Then you can rename all the functions to remove the 2. It’s quite a lot of work.

And the biggest problem is you have to do that before you even know if it’s going to work the way you want it to (as in, run the software and test it).

Clojure avoids this scenario because functions only tend to depend on very small parts of an entity map. For instance, they may only care about one key. Any changes to the entity that don’t remove that key are still going to work. You don’t have the cascade of changes. That’s why we tend to say that types are coupling.

I can imagine things are much better now. There was something new in ghc, where you could defer type errors to runtime, which could make things easier to compile and run small sections of your code.

Eric

4 Likes

Very interesting data about importance of different practices. But do you have any proof, links, references, data, statistics… anything?

For me its clearly the tooling and the backing by all the java libraries available.

Have you tried Atom, in case you are not a fan of Emacs?
Can you be more concrete about what libs you specifically are missing? Haskell offers web servers, streaming libs, access to AWS services, a build system, test and benchmark libs, cryptography, etc. Anything concrete that you need and that simply isn’t there?

Also in Clojure some of the tooling is not available. For example the type checker and the type system is lacking and one could argue that this is an extremly important tool. For example, when you have a bigger software project and there is a bug in an http request that you were making then you could ask your local search engine to show you all „functions” in code that possibly can make http requests. This will shrink the code size that you have to study to maybe 2-4 places, within seconds. So this kind of tooling is also of value for some.

the docs most of the time were very good

Here I would disagree. For tons of libs the documentation in the Clojure world is dramatically much better. In Haskell you have always the type signature and that is possibly the most important piece of information, generally. But nevertheless: seeing examples and having a reference is super important too.
Clojure libs typically have really nice documentation. I look at the readme and after a short time I can begin to work with this.

The fact that Clojure gives you full access to all of the JVM eco-system

You could as well have said that you prefer Haskell over Clojure because it gives you full access to the GHC eco-system.

Anyway, with Frege and Eta you have two Haskell compilers that also give you full access to the JVM eco-system, plus you can mix it with Clojure projects.

The fact I can then pivot to running on JavaScript hosts when needed.

That’s really good though with PureScript you can do the same. Essentially PureScript is like Haskell. And: with GHCJS you can also compile to JS. But this is indeed still very rough and I only mention it for completness.

Lisp syntax

You can use parens and prefix syntax in Haskell too.

the consistency

Yeah, with the exception of macros which introduce new language rules that you need to know/learn.

extend it easily with the use of Macros

The majority of macros helps introduce lazyness and are simply not required. In Haskell you can abstract most boilerplate code away. And because of the richness of type information the compiler can create boilerplate code for you as well. For the other cases you can use Template Haskell and have macros. But I admit that they are easier to write and more accessible in Common Lisp and especially Clojure. The Haskell macros require more learning time and more time to write. But they are more often correct and easier to refactor.

REPL integration

It’s called GHCi. When doing Haskell I constantly switch back and forth between the code buffer and the repl, and I can not say that it is dramatically different from CL and Clojure.

I’m not a fan of being restricted to only use lazy constructs.

Haskell is lazy by default but it’s trivial to opt-in to strictness. The other way it’s more complex. Defaulting to lazyness can be considered as advantageous.

Static Typing overhead. Always having to think about types, and their definition

You need to think about the types constantly anyway in Clojure too. Also one can argue: in dynamically typed languages you need more thinking ahead because you get into trouble if your implementation is not good. You can not refactor easily because it’s difficult to identify all callers and places that require a change. In Haskell it is ultra cheap to refactor code so you do it all the time. You can explore more because it’s fine to make mistake and choose bad data structures. You can later on change things when you learned more about the concrete situation at hand. In dyn langs this is a major undertaking, reminding me of merges with cvs (vs git).

I’ve never had a lot of type induced bugs in practice in Clojure

Nonsense. We all do it all the time. Most of our major bugs in our Lisp systems were due to errors that would have been caught at compile time by Haskell. Not all of them are type errors though. But for example you may use keywords to represent certain constants in Clojure. They are not better than strings, if you have a typo in them you won’t get a notice from the compiler. In Haskell those would be values and it’s really unlikely that you can get something wrong into your code.

And don’t forget that you can encode very many things in Haskell into types that would not be type errors in other languages. For example: if you write code that would allow an XSS attack at run time then it’s a compile time error in Haskell.

I find the purity compromise of Clojure to be more practical

I have to disagree strongly. Purity is so very useful that I can’t stress it enough. A whole class of errors exists in impure code. Knowing exactly which few places in the code can produce such erros are immensly helpful. Also you can restrict effects into certain groups. You can easily say: hey in this „function” the only side effects I allow are uploading files via FTP and doing HTTP Put requests.

If doing an HTTP request is the only side effect you are interested in then why not tell it your programming language explicitly? It will immediately remind you if you suddenly change your mind and ask you if you really want to do this.

Monads for side effects work well, but it confuses me more.

Of course they confuse you, simply because you are not knowingly using them and thus lack training. As soon you start working in a Haskell company you will master them very quickly. Besides that you are using them 100x per day. It’s only that you don’t realize it. For example Clojure’s vectors and lists are monads and the use of let and for.

I’d rather know that I’m just breaking purity to have side effect and control it myself how I want.

Would you please explain how Haskell prevents you from controlling side effects?
And can you please explain how you control that users of your library do not use side effects? I would rather say that in Clojure you have no control over this but in Haskell you do.

I prefer prefix notation

In Haskell you can express very much of your code in prefix notation, including operators such as +.

I do not like starting from type signatures as much of what I do involves exploratory programming in which I discover the types through experimentation

You always start with the types. Otherwise you could not use values in many function calls. If an argument could have any type then there is a very restricted set of functions you can call on it. For example calling identity is fine.

Now the real challenge arises when you start changing types. In Haskell it is trivial to change everything and still have a functioning program that you can push to production. When you use a dynamically typed language you need more thinking about types and interfaces because refactoring is more challenging. In Haskell it is easier to explore things because it’s fine to make mistakes and choose the bad types. So I think exploratory programming is actually easier in Haskell.

Hmm, I would say that if it is easy to read then you spend less time on it.
Imagine a function that adds 1 to a number: (inc n) or (+ n 1).
But you could as well do it like this: (+ n 1 -1 1)
Or like this: (+ -1 n 1 1 -1 1)
Or you could make it much more complex, call literally tens of thousands of functions, fill millions of lines of code, and in the end it only adds 1.

In code that’s „easy to read” you understand faster what’s happening and thus can add new features faster or fix bugs or simply make use of that code.

Then why not just learn BASIC and use that to write your software?
If programming is your hobby then it should be fun to learn new abstractions that let you express code in a nicer way. If it is your profession and you like it then it should also be in your own interest to learn new things. One way to spend time is to look at libraries and „learn” their API. But libs come and go pretty quickly. Good abstractions stay for a while. Why all the hassle about functional programming when state mutation is so easy to learn in C++? You already spent some time to learn how to reduce the amount of state mutation by learning Clojure. Why? Clojure tries to make state mutation more explicit. Why?

Monads are not magic but they can potentially make you more productive. It‘s like programming against an interface (or a defprotocol in Clojure terms). Your code is more reusable. Those strange academics were working for decades on finding out more about how powerful abstractions work. Monads helped to realize that working with computations that can fail (with or without useful extra information) share concepts with mapping a function over a list or running side effects or not having to explicitly pass parameters around.
Those all sound like totally different things, yet they share properties. When you realize this then you can write one single function that handles all of these situations. This is reusable code.
And Haskell code is ridiculously composable.

Take this one web project as an example. It was written in Groovy, 40k lines of code. Groovy is maybe not as powerful as Clojure, but you can express a lot in 40k Groovy code for sure. The issue was that their software was very buggy and slow. It was using all 64 cores at 100%. One issue they had was for example that they were sending emails during DB transactions. This was fine until there was a rollback – and the email has already been sent.

So they translated this app into 8500 lines of Haskell. Funnily the statically typed language which forces you to write so much boilerplate code resulted in a shorter solution than the dynamically typed version. Plus they could now run their app on just a single core. And many bugs were gone. If now somebody would accidentally try to send emails inside a DB transaction it would be a compile-time error.
I believe it was this 20 minute clip in which someone talked about this:

1 Like

You can defer type errors to runtime. I’m not sure if Haskell devs use this a lot though.

https://downloads.haskell.org/~ghc/7.6.1/docs/html/users_guide/defer-type-errors.html

Could you please explain why you think so?
What aspects of Haskell cause developers to think about functions first and not about data structures?
What aspects of Clojure reverse this?

Without providing any argument you could as well have said:

Haskell: Think about data first
Clojure: Think about functions first.

There is no serious study that can answer the important questions. Those studies unfortunately have to be very limited in scope. Then again I have no problem to believe that first results may typically come faster when using a dynamically typed language. The first results will however possibly have more defects though.

A too generic statement. By grouping all statically typed languages into one bucket, though there are big differences between them. Badminton and mixed martial arts can both be considered sports but the sportsmen have not much more in common than their desire to breathe.

Static typing à la Haskell is about changing code and being confident that the program will still work. When functions get more or less arguments and/or the type of the arguments changes then all callers need to be updated. Computations that can fail (i.e. return nil or throw an exception) need to be handled if you want to be sure to have a correct program. Just think honestly about how often you’ve done (:my-key hm) without checking first if that key exists? Or how often you took the first element of a vector without first checking if the vector is nil. If you don’t do this then your program my crash at runtime, i.e. throw an exception. Just search through your code base at work for „first” hashmap accesses and see how often you continue working with the value without having checking if you have one. Even if this does not result in a bug now: in the future a colleague might change parts of the code and suddenly a certain key won’t be present anymore.

Or even worse: you push elements onto a queue and only much later some other thread reads the data from the queue. And now there is suddenly a nil and that thing crashes and you have no idea where that bad value has been produced.

Then your list about things that help to reduce the defect rate. The first two are about the type system. Strong typing helps, agreed. And runtime checks are worse than compile-time checks. In Haskell GHC defaults to the compile-time checks but you can have the same behaviour as with Clojure by deferring them to runtime. If you start working with Haskell you will likely not do this.
Then you mention functional programming and later declarative programming and immutability and. Those all can be summarized (very roughly) as purity. The essence of functional programming is that you don’t cause side-effects and don’t mutate state. First class functions are also available in Java, but they don’t make Java a functional programming language. JavaScript also has first class functions – is it a functional programming language? Clojure is considered as one by many because it defaults to immutability, which is a subset of purity.

The others are about testing and that is indeed ultra important. Haskell can still help a bit here because it is trivial to create fixtures. In our Lisp project we didn’t start from the beginning to write tests and now have classes with fields of classes that have fields of classes that have fields of classes, etc. We can not simply write tests for such instances because we can’t create them for tests. We don’t know how. In Haskell this is totally obvious and can be done quickly and sometimes automatically. Also the compiler will help you to remove a whole class of bugs completly by proving it mathematically. This means that you can not accidentally forget a certain test case of a specific value.

Of course it does. In one Clojure project one of our test cases failed and I and a colleague were trying to debug this. We saw how a function was called that got passed an argument. It took a few moments to discover that this was a function argument. We wanted to pass in a function for testing but it was totally unclear how many arguments were required and of what type those args had to be. We wasted half an hour for something that would have been a 2-second thing in Haskell. This is where static typing may help. In complex situations, if you have long-lived software and want to change it.

Yeah, because type information was missing it was more difficult to read the code and it took much more time. Regularily I spend hours to detect where type errors are located to fix them.

Emperical data in software development is at best just hinting something. They don’t go into real projects in companies and check into what bugs they run and how they fix them, and what kinds of developers can fix them quickly.

And can you please explain what aspect of static typing makes you less productive in what circumstances? I have collected a few examples but I wonder what you can come up with.

I see that you are having lots of feelings. And you have them despite the fact that you are not really experienced with Haskell. Without understanding the architecture I know that I can go to a random location in code and prevent the insertion of a certain key/value pair into a hashmap and know the software won’t crash in an exception. Because I know that the caller of “lookup” already handled the case that the key wasn’t present. Initially the dev may have forgotten to do so, but Haskell reminded him in the second while he was still typing the line.

Yes, this is good. Years of Haskell experience have probably qualified you to give honest assessments about this topic.

1 Like

Not sure if you’re a very junior person with a blog post level understanding of these topics or a random troll. Assuming the latter, I’m not going to engage with this rubbish; if it’s the former you may want to consider why I suspect the latter.

1 Like

Yes, types can be helpful. Especially in larger code bases with many developers.
But types are a solution for a problem that’s created by types in the first place!

I.e. the tight coupling. People tend to build humongous typed things: Why?
Because they can! They are encouraged to by the type system.
The technical debt accrued by this is much more difficult to see, but I assure you: it is there.

You might think of refactoring being ‘easy peasy’ in statically typed languages, but in reality it is not that simple and it often requires non-trivial changes to code you didn’t even plan to touch.
There is a reason that older, larger code bases resist meaningful changes after a few years. It’s the inertia caused by lack of proper abstractions and lack of loose coupling.

I’ve changed my opinion on this many times, but having used JavaScript for a few years now and having returned to my Lisp roots with Clojure, I can see real, measurable productivity gains with dynamic languages.

I can also see the whole class of errors you are getting back when letting go of strict typing.
And it certainly is a trade-off.

Yes, you can build modular and loosely coupled code with types too. But are we doing that?
With Clojure, I can push these concerns to the boundaries of my code where they belong.
I can go full spec on my external interfaces to get back some of the benefits types afford.
But I’m not being held back by having to typify everything.

4 Likes