A Festive Accident
For quite a few years, I’ve taken part in Advent of Code. It’s excellent, and you should definitely give it a go if you haven’t seen it before – twenty-five coding puzzles steadily increasing in difficulty. I’ve recently been using it as a way to explore new programming languages, so November 2022 I was playing around with a few different languages:
- Common Lisp – a friend had raved about it the year before.
- Elixir – I’ve dabbled, but never coded in anger.
- Rust with WASM – I used rust last year. It would be interesting to tie it into a website this year.
I’d been re-reading through a book on Elixir and was trying to get my head around the syntax. Then I stumbled over this tweet:
I then rashly replied with this:
I had pre-ordered Effective Haskell on a beta program, but had forgotten about it until now. But, I’d said it, it was out there, I was doing Advent of Code in Haskell for 2022. And it went really well! For various reasons, I only completed sixteen days in December, but I learnt a lot of interesting things about Haskell and my coding practices.
You can see the messy things that I got up to here. (Don’t look at day 16, it’s a bad implementation and far too slow!)
What Did I Learn?
1 Reading blogs and books isn’t a substitute for coding
This is true for any language. However much you read about technology, there’s nothing like actually using it. For me and Haskell that meant that my first few days the problems were fairly easy, but I spent a lot of time setting up my environment. Beyond that, a lot of trying to work out how to structure a non-trivial project, organise code into sane pieces.
I do AoC in a more verbose style than many, but that suits me for what I’m trying to learn from the exercise.
2 The Haskell ecosystem is rich and helpful, but can be academic
This is definitely something to be aware of, particularly if coming from a language with a large community or major corporate backing.
For one example, on installing I was pleasantly surprised to find a very nice bundler called ghcup is now available. It automatically installs GHC (the compiler). It automatically installs a language server, for IDE support. Fantastic – except that the defaults of these two pieces (when I installed) were incompatible.
I spent a few days living without language server support, before digging into message boards and realising that I would need to recompile the language server by hand. This wasn’t hard – but be aware that you’re likely to be working a little bit closer to the cutting edge than some other languages. Pros and cons.
Aside: once I had the language server working, I was quite happy with how it worked within Visual Studio Code. I found the errors and suggestions useful, and worth setting up.
The story for picking packages is similar. Some are excellent and well documented, some are excellent and well documented but written for the computer scientist academic. I stumbled a little for this reason over picking a parser combinator library. I accidentally picked something initially that had a good tutorial, but was no longer supported – and triggered versioning conflicts. When I shifted to attoparsec things worked better, but I found it hard to use quickly. It’s powerful, but it was difficult to cut corners and get something messy working. Again, pros and cons – the power comes from the strong theoretical background, but you need to learn the theory.
3 … But there are excellent books around
For what’s often seen as a niche and obscure and difficult language, there are a lot of high quality texts. I’ve already mentioned Effective Haskell, but there are a good few other options, including the ageing but free Real World Haskell. I don’t know why, but there’s a strong line up of quality Haskell books around.
4 The customisation isn’t as scary as it sounds
I started reading a book on Haskell a few years back, and was confronted with initial chapters was full of picking custom preludes and language extensions.
The prelude is sort of like the Haskell standard library, very very roughly. Language extensions are flags controlling possibly experimental language features, which may not be included in all implementations. This sounds scary compared go or python where a novice just uses the default language as supplied.
In practice, it’s not that scary. There is really only one implementation which is widely used (GHC), and the extensions recommended are stable. They’re not in the Haskell standard yet, but that’s more due to the standardisation process than the quality.
It’s also incredibly easy to opt into different language extensions. e.g. For flipping to a different string representation, include the relevant .Text packages, switch on the OverloadedStrings extension, and all works smoothly. I ended up using this extensively with no pain.
This is one of those issues where Haskell both benefits and loses due to its experimental academic background. On one hand, customisation is common and almost expected. On the other hand, the language designers have made the process very smooth.
5 Programming with types
Programming with types is fascinating, and Haskell really leans into this by default, more so than most languages I’ve used. (I’d suggest even more than some others in the ML family like Ocaml or F#.)
I’d often sketch out type signatures for functions before writing them and then be able to rely on the compiler to help me write them correctly. In Haskell typing rarely feels like tedious book keeping, but instead a way of describing your intentions.
6 Embrace laziness
A long time ago, reading about Haskell laziness my immediate reaction was: that’s cool, it only computes what it needs to, it can be more efficient and faster. I’ve revised my opinion, and think this misses the point.
What I realised this time around was: laziness as a core language feature impacts the way that you design your program.
As an example: for several days I kept wrestling with writing something like a for loop. I knew my initial state, I knew how to move to the next step, I knew when I wanted to stop – but how could I encode that as a finite length sequence?
In practice, I found that you don’t. You code the sequence as an initial state, a production function – and that’s it! You have a lazy but infinite sequence. Then when you want to find the answer you drop or take from that sequence to get to the step that you want. I came to find this division between defining the sequence, and finding a value from that sequence quite elegant.
7 Performance isn’t as scary as it sounds
Again, this is something that looks scary. It’s easy to read one too many articles on performance in Haskell, get lost in iteration after iteration of code tweaks and compiler twiddling, and despair of ever writing performant code in Haskell.
For most code you don’t need to. Default performance is fine.
Certainly if you’re doing high frequency trading or large scale number crunching, you’ll need to learn all those tricks, but for many many purposes just letting GHC do its default thing will be fine.
Small caveat – do things idiomatically, and performance will usually be fine. If you don’t work with the language, accidentally re-allocate memory repeatedly, then yes – things will hurt.
I think there was once that I had to jiggle things around to avoid laziness getting out of control. (Very crudely – GHC by default was delaying evaluation over a vast data structure, and then trying to do the whole thing together. Like trying to swallow a lettuce, rather than leaf by leaf.)
8 Take as much of monads as you need
This is an interesting one.
You need monads in Haskell. You can’t avoid it. You want file access, you want to interact with the world, you want output – you need monads.
However, what you need for that is quite small. Work through tutorials, learn enough to do what you need – but then only pull in things as you need them.
My monad usage started basic, but then over the few weeks I started playing with different operators, avoiding do notation, even writing my own monad instances. (The instances I wrote were probably unnecessary, but a good exercise.) It’s a fair old
I played around with the array monads when appropriate, and found the Effective Haskell descriptions here particularly helpful.
The mathematical underpinnings are enlightening, but I didn’t really have the chance delve into – see, for example, the Haskell Typeclassopedia. The promise of considering problems more as relations between mathematical structures is deeply alluring, and I’d love to get into it more on meatier problem (AoC intcode interpreter?) – but you don’t need any of this to get started and be productive.
9 Libraries are decent
In a positive way. The Haskell ecosystem is sufficiently large that there’s usually a package available that does what you want.
It may sometimes lack the latest features, and you may have to work out which of five different libraries is the best choice for you – but it’s usually out there. This state of the haskell ecosystem review is insightful and honest.
10 The Module System left good impressions
Not exceptionally different from other languages, but I’d like to highlight as good and obviously being developed.
You can define packages, exports, dependencies all in nice sane ways. I didn’t thrash this part of the system particularly hard, but it was quite pleasant to use.
11 What I didn’t learn
There are a few things that I didn’t learn:
Testing: This is an embarrassing one. I know that Haskell has excellent testing libraries. I know that Haskell has the well thought of QuickCheck which showed the path for property based testing. Yet, I didn’t touch any of them. I think I had burnout from learning so much else that I didn’t have the motivation to add tests. (I did do proper test suites in rust last year, so this one is a bit annoying.)
Monad Transformers: I know enough to know what they’re for and how they work, but didn’t have a strong use case for using them – so didn’t. I therefore can’t give a good overview or comment on usage. It does feel that they’d quickly become essential on some level for production coding.
Algebraic Data Types: I didn’t learn this, because I already knew it. I’ve used algebraic data types in F#, in rust, even in Elixir – and I always miss them in languages that don’t have them.
Template Haskell: Haskell is not Lisp, so there’s not the same emphasis on macro type programming as a core language feature. I also struggled to find good documentation, and the general impression I was left with was that it was all a bit scary. This may be unfair.
(EDIT: After posting I was pointed to this template Haskell cheat-sheet. It’s exactly the sort of thing I was looking for, and cross-references some tutorials!)
Summing Up
Coding in Haskell was a good experience. Even now, looking back at my code it seems pleasantly succinct, direct and descriptive. There are lots of features that I liked besides the ones above. Lot’s of good features that aren’t unique to Haskell but I appreciated being available – like the rich module system.
The main thing that I learnt from writing in Haskell is that it genuinely has a number of distinct features, and a distinct take on modelling.
Would I use Haskell in production?
- There’s a whole world of detailed debugging, testing, and performance that I’d want to be comfortable with before recommending.
- There’s the hiring question – hiring or training good Haskell developers is likely to be slower, so if I needed to grow a team fast that might be a problem. There’s the blind leading the blind problem – if you’re not lucky enough to get a Haskell guru in house, then you need to work doubly hard at building quality.
- Haskell has good libraries, but won’t automatically get libraries, as you might be able to rely on in javascript, or python, or C, or,… So, you might inevitably need to supplement your Haskell.
- Haskell isn’t a magic bullet. No language is. Go into the question with your eyes open, weigh up the pros and cons, and maybe the pros will outweigh the cons for your use.
Having said that, Haskell, in my opinion, doesn’t have any major language flaws for general programming, and is unique enough to warrant it’s place on the list of languages worth knowing.
Elsewhere and Next Time…
I’ve been doing lots of JSON parsing and interpretation. Navigating deep data structures in interesting. Playing with both Ocaml and Lisp for investigating interactively:
- Ocaml – fast, interesting, some unique features, surprisingly few texts, and a single implementation. Strongly driven by a few industrial interests.
- Lisp – lots of documentation, amazing interactivity, surprisingly ahead of its time in many ways. Standardised language with many implementations.
I think they’re very interesting, very different ecosystems to compare, so this may be the next blog post.