Two years ago, I starting learning PureScript. I had been intrigued by purely functional programming for some time but had failed to learn Haskell once or twice. PureScript seemed to be a kinder, gentler introduction to this world while retaining the fundamental properties of pureness that made Haskell intriguing to me. As part of my learning process, I rebuilt a slack bot1 I had previously written in go.
Once I had learned PureScript and become more comfortable with purely functional idioms, the next logical step seemed to be learning Haskell. I was surprised to discover how much Haskell I already knew from learning PureScript, but core features like laziness (PureScript is a strict language) took some getting used to.
I decided to finish my Haskell learning experience by rewriting my slack bot2 once again, this time in Haskell. In this post I’ll compare and contrast my experiences writing the same program in Haskell and PureScript. The application I built was “real” enough to have some interesting design challenges. They included:
- Exposing an HTTP endpoint
- Parsing and generating JSON
- Full-text search
On to the comparison!
Strict vs Lazy
Haskell is a lazy language while PureScript is a strict one. I expected this core difference to manifest itself constantly when writing these applications, but in reality it rarely came up. I had predicted a lot of banging my head against the wall dealing with laziness bugs but it just didn’t happen.
I will say that I generally prefer PureScript being a strict-by-default language. When laziness is required, there are plenty of ways to get it, but it is always explicit.
While not directly related to strictness, a pain point on the PureScript side that I didn’t experience in Haskell was stack safety3. In PureScript, it can often be confusing to determine if the operation you’re using is stack safe. When these operations aren’t stack safe, the errors that are produced can be confusing and hard to track down. I found myself struggling with stack safety in PureScript far more than I struggled with laziness in Haskell.
Tooling
A year or two ago, I would have simply said that PureScript has incredible tooling and Haskell does not. Thanks to the amazing work on the Haskell Language Server project, this gap is starting to close.
Regardless, PureScript still has far better tooling. Spago is an incredible build tool that offers many of the same features as Stack while remaining far more user friendly and PureScript’s language server is excellent and easy to install.
However, PureScript is currently struggling on a few fronts. First, the package ecosystem is moving from bower to a registry hosted on Github. The resulting registry seems to be moving along nicely and I believe the result will be incredible for the language, but the current in-between state is unfortunate. I am glad the core maintainers are taking their time to design this registry well and I firmly believe that this will be a strong positive for the PureScript community in 6-12 months.
Second, PureScript doesn’t have a great option for a formatter. While
purty does exist, it seems to be mostly in
maintenance mode and many of the formatting choices are frustrating for me.
Specifically, the automatic removal of blank lines within functions and the
addition of newlines for let
assignments in do
blocks both hamper
readability and author intent. On the Haskell side,
ormolu was easy to install and
“just worked”.
Language Features
It’s not a controversial statement to say that Haskell has far more language features than PureScript. It’s also not a new observation to say that it is challenging to determine which of these features one should be using and what extensions one should enable to use them. Here’s the list of default extensions I have enabled for my project:
- DataKinds
- DeriveGeneric
- FlexibleContexts
- FlexibleInstances
- GeneralizedNewtypeDeriving
- InstanceSigs
- LambdaCase
- MultiParamTypeClasses
- NamedFieldPuns
- OverloadedStrings
- ScopedTypeVariables
- TypeApplications
- TypeOperators
I found Haskell’s deriving capabilities to be more powerful than PureScript and led to reduced boilerplate, especially when creating my application monad. I also like that Haskell’s type class instances do not require names. The names required by PureScript add very little in terms of readability or author intent.
By far the biggest difference I felt between the two languages is the way they deal with records. Again, this isn’t a new observation, but it can not be overstated how much better PureScript’s records are than Haskell’s. Records based on row polymorphism are a joy to work with, as is having a dedicated syntax for creating, updating, and accessing records. GHC does have an accepted proposal for adding record dot syntax which will solve many of these problems, but I think the underlying implementation based on row polymorphism will continue to give PureScript the edge here.
Compile Times
When I first learned PureScript I compared its compile times to other statically typed languages like Rust, Go, and Java. I found it much slower than these other languages, though incremental rebuilds were generally quite fast. I assumed upon moving to Haskell that the situation would be comparable or better. I was very wrong.
Compilation times in Haskell are significantly worse than PureScript, often by an order or two of magnitude when compiling a project’s dependencies. I say this less to rag on Haskell (this seems like a challenging problem to tackle), but more to applaud the PureScript community for the work they’ve already done on this front.
Ecosystem
Haskell has a far larger ecosystem of packages than PureScript – kind of. In terms of Haskell and PureScript in isolation, Haskell has a vastly superior collection of packages and the quality of these packages is generally very high. However, PureScript has done a great job of porting over many of the best Haskell packages.
Additionally, PureScript gives you access to the entire ecosystem of JavaScript packages via FFI. While many in the community find this to be something of a disadvantage, from a practical perspective it is fantastic. As an example, when building full-text search for Haskell, I ended up using the full-text-search package. It was fully featured and comprehensive, but required quite a bit of code to get working.
On the PureScript side, I built a simple FFI wrapper around elasticlunr. While I understand that the JavaScript ecosystem has packages of variable quality, it does have lots of packages solving many problems and PureScript’s FFI makes it extremely easy to leverage this giant ecosystem in a safe way.
Web Framework
For my PureScript application I chose the HTTPure. It was light-weight and easy to use while feeling idiomatic. This was a relatively simple choice because there are few options for server-side frameworks within the PureScript ecosystem that included the features I needed (specifically middleware).
On the Haskell side, the choice of web framework was more complicated. I wanted something small and light-weight, but with the ability work within my custom monad stack for my application. I ended up using scotty, but the default middleware solution doesn’t operate within your application’s monad stack, so I needed to explicitly provide middleware-like-functions for each endpoint in my router.
At the end of the day, this was mostly a wash between languages, but I expected the Haskell ecosystem to be far more mature in the backend HTTP server space.
Deployment
To deploy my PureScript application, I used
ncc. While this required that I had node
available in my deployment environment, it made everything else easy. The ncc
tool produced a single, self-contained JavaScript file that included all of the
application code along with required dependencies. I then simply scp
’d this to
my deployment environment and ran it with node
.
On the Haskell side, I used stack’s docker support to build my application’s
executable within a container that matches my deployment environment (debian),
and then shipped the resulting executable via scp
as well. The executable was
completely self-contained.
Overall, both approaches felt equivalent in terms of ease. On the PureScript
side, it is a bit frustrating to need node
within the deployment environment.
On the Haskell side, there was the added complication of having to use docker
for cross-compilation. Overall, though, both experiences were reasonably nice.
Conclusion
In reading over this post, I worry that it feels like I’m picking on Haskell – I’m absolutely not. I’m very aware that PureScript is heavily influenced by Haskell and is standing the shoulders of giants. PureScript was able to learn from some of the mistakes of Haskell and make choices about intentional departures from the Haskell ecosystem that better fit the intended use cases of PureScript.
I found the experiences of learning and using both Haskell and PureScript very rewarding. I will admit to being surprised at how well PureScript compared to Haskell in the server-side HTTP space, given that its primary focus is currently on the front end. I think both languages have a bright future and I’m excited to follow their continued development.