How I learned Haskell in just 15 years
by Evan Silberman, Duckrabbit Solutions
Haskell is a programming language invented sometime in the 20th century by Scottish logicians as a prank.¹ Fifteen years or so ago, for reasons I can no longer remotely recall, I started learning Haskell. Now, I have finally written a useful program in Haskell, and I am pretty sure I can do it again, if I ever need another computer program.
I don’t know how I learned of functional programming in general, or Haskell in particular. I was following why the lucky stiff’s projects by 2006, and reading Leah Neukirchen’s pioneering tumblelog Anarchaia, and either of these sources may have exposed me to the world beyond OOP. Leah posted a link to Pandoc on Anarchaia in December 2006, which could’ve been when I first learned about my favorite piece of software and the language it is written in.
By my first year of college, in 2007–08, I had certainly heard of both functional programming and Haskell, because I decided I should learn how to use them. I contrived to do an independent study in functional programming with the help of my next-door dorm neighbor, who was a Common Lisp fan and Emacs user with four LCD monitors. My textbook was Programming in Haskell, by Graham Hutton, which I assume was recommended somewhere on Haskell’s web page. I don’t recall if I got any credit for the independent study, but I probably installed GHC successfully. God knows how I managed that. I could’ve been using Fink, or MacPorts! I bet I learned about map and foldr. I probably heard the M word. I didn’t really learn any Haskell.
Later that same year, Real World Haskell by Bryan O’Sullivan, Don Stewart, and John Goerzen came out. It had “real world” in the name! I got a copy at some point, and even read some of it. I would’ve been using the same computer, so if I was in fact trying to write any code, I probably didn’t have to install GHC again. I was not (and am not) all that good about sitting down with any kind of technical tutorial and following along doing the exercises from beginning to end. And it turns out that flipping through a book without doing anything with it isn’t too useful. Thus I didn’t really learn any Haskell.
In 2009, I got a netbook. Remember netbooks? Some of you weren’t even born! I got an Asus Eee of some model number or other and put Arch Linux on it. Probably because of my helpless fascination with Haskell, I chose Xmonad as my tiling window manager. Your Xmonad configuration is “just” a Haskell program, and you effectively recompile Xmonad with it to reconfigure your window manager. Isn’t that neat? So I finally had an environment to write some Haskell where I had a goal in mind. I don’t think I learned much about programming in Haskell from customizing Xmonad. The example config was extensive, and I believe I customized more by subtraction than addition, cutting my config down to one full-screen and one split layout. Set up some keybinds. Put something in a status bar. That kind of thing. At some point I stopped fiddling with it, and kept using the laptop. Success!
For some time after college, outside of perhaps a couple casual stabs that didn’t go anywhere, I didn’t attempt to make use of Haskell. I did, however, frequently use software written in Haskell. Possibly just because it was written in Haskell, even. Certainly that was the reason I tried out hledger instead of ledger when I tried out plain-text accounting for the first time around 2011-12. (It turned out to be too depressing to track my personal finances while I was unemployed.) It may have been a contributing factor in my decision to try Darcs as my first distributed version control system during college, instead of the still-emergent Git. (I haven’t used Darcs enough in recent years to tell you that I still think it’s better, but it might be!) This kept Haskell on my mind throughout jobs where I was using Ruby, Perl, Python, JavaScript, and lots and lots of SQL.
I finally made significant progress towards learning Haskell by learning Elm. Elm bears a deliberate resemblance to Haskell, and its compiler is built in Haskell, but where Haskell materials can overwhelm the casual reader with generalized abstract nonsense, Elm’s embrace of constraint helped it present a friendlier face. In 2016 Elm 0.17 came out and consolidated its focus on The Elm Architecture and interactive browser applications. This was a big moment for Elm’s mindshare, and it filtered into my consciousness at the right time.
At work at the time, our newest and freshest JavaScript stuff was built with AngularJS. I could barely understand how it was meant to be used, and I couldn’t evade feeling like we were using it wrong. It seemed like an unprincipled mishmash of states and templates and mutations. Two-way data binding confounded me, and any separation of logic from presentation could be easily escaped, making it even harder. I selected Elm for a standalone dashboard page providng a friendly, filterable view of some data pre-aggregated from various sources. Building an Elm app following The Elm Architecture hooked directly into what my tiny brain was capable of: you have a state, the web page is rendered as a pure function of the state, when you interact with it it sends a message back to your app and you process that message and produce a new state. A nice little circle of arrows going one direction. I built a whole interactive client-side browser thingy, albeit a read-only one, by myself, something I hadn’t done before, and it was really pleasant.
Elm constrains the programmer to building one kind of application with a restricted set of abstractions on a single platform.² Operating within those constraints, with a real project to work on, was a great way to learn not just a language but also the more general, language-independent principles that the language and platform are built on.
An illustrative example is the matter of encoding and decoding JSON data to domain types. In the mainstream dynamic languages (Perl/Python/Ruby/JS), dictly-typed programming is common to begin with: it’s quick and uncomplicated to build business logic around dictionaries with implicitly-expected sets of keys. Once you’re calling an external API and decoding it as JSON, it’s quite natural to just take the dictionary you get from that and pass it around without inspecting it at the point of decoding. When your app crashes because a key is missing from that dictionary in certain situations, or contains an unexpected null, you’ll probably add a null-guard at the point of use and move on with your life.
Elm’s JSON support, by comparison, nudges you to explicitly decode JSON responses into the domain-modeled algebraic data types used elsewhere in your application. There’s no support for just decoding your request into a heterogeneous dictionary with arbitrary depth: you must map your response to something, and you must handle the cases where your decoding fails when your data doesn’t conform. What you end up with is a clear boundary in your program between the outside world and the data you actually work with.
Since I had a blast with Elm and I knew that I was learning stuff that should map to Haskell reasonably well, I started sniffing around once again for a way in. What I found was Shake, a Haskell library for building build systems. During a talk session at a conference I had to attend but had barely any reason to pay attention to, I started hacking together a static site builder for the directory of Markdown documents my colleague and I had started accumulating as internal documentation. As with my Xmonad config years earlier, I didn’t have to get incredibly familiar with Haskell to follow Shake’s bread crumbs to a functioning solution. This was good for my ability to produce a satisfactory documentation site but it also meant that even with my comprehension boosted by Elm I still doubted that I really understood Haskell.
Somewhere during this whole period of my life I watched Gary Bernhardt’s seminal talk Boundaries, which makes a case for employing immutable data and pure functions in the context of object-oriented dynamic languages. Obviously I cottoned on to the idea. My next job’s main language was Python, which had recently grown dataclasses, and I structured a few different chunks of code around them, even inside legacy codebases which were mostly factored quite differently. I started saying that I like to write Python as if it were Haskell, despite still not having super-meaningful experience writing in Haskell.
My favorite instance of this approach was in the context of an application managing the repayment schedules for an internal loan portfolio. An earlier, departed developer had implemented the schedule generation process in a dictly-typed style, distributing the business logic of the loan schedules across various functions, in a bit of a mishmash with the web application request handlers and database update queries. When the customer brought up inconsistencies or errors in how the schedules would get updated in response to payments or other changes, it was a subtle process figuring out where the logic error even was and how to durably correct it.
When I had finally had enough of this, I sat down and rewrote the repayment schedule generation from scratch. The basic task was, given a sequence of transactions that have already happened on a loan, output the scheduled amortized payments until it is paid off. Various quirks of the loan program, such as borrowing additional principal after repayment had begun, had to be accommodated. I defined a model for the sequence of events that can happen to a loan, independently from our database schema, and made each of these operations a dataclass. I wrote a function (imperative in implementation but stateless) to generate the remaining principal and interest payments for a loan with given terms and existing payments. To dig up edge cases, I wrote Hypothesis tests³ that basically ran the schedule function and checked that the final balance on the loan after applying every scheduled payment was zero. (That worked; I fixed some bugs.) Finally, off in a completely different file, I wrote the code that translated this data structure back into the database entries that stored the schedule for use elsewhere in the web application.⁴ It all worked really well and I could actually understand what this crucial part of our business logic was actually doing. It’s now been well over a year since I did this work and I’m still pretty delighted when I think about it.
As far as programming in Haskell goes, my next milestone came when I read Jack Kelly’s brilliant article Text-Mode Games as First Haskell Projects. It lit a fire in me and I sat down to try it out. I didn’t get extraordinarily far, but I wrote a tiny game-like creature that could spawn monsters for the player to encounter and attack, or run away from, complete with randomized hit points and attack damage. I still didn’t feel like I was ready to be useful with Haskell, but making a tiny effort to at least have fun was refreshing.
The real breakthrough came after I got laid off in October of 2023 and went freelance.
Duckrabbit Solutions offers a range of software, systems, and data engineering services. I have a particular interest in investigating and documenting legacy code and helping small or inexperienced teams get a handle on stuff that’s big, old, and critical. If your organization has something scary and weird that you need help understanding, whether it’s an internal app, a legacy database, or a subtle business process, send me an email and tell me about it.
To my amazement and gratitude, my first client came calling quickly, thanks to 10+ years of posting on a niche messageboard. I negotiated an hourly rate so I needed to start tracking my time. I threw together a tiny schema for time tracking in SQLite and wrote a hasty shell script to clock in and out with. I handled my reporting needs for submitting my invoices by composing appropriate queries in the SQLite repl and hitting the up arrow. I knew I wanted to write a real program to handle this stuff, and I started writing it in January. In Haskell.
I succeeded. I wrote an honest-to-gosh real, useful Haskell program. It parses command-line options and talks to the database, like a real program. I can clock in and clock out and backfill hours I missed tracking. It converts UTC times to my time zone. It can print hours reports grouped by day or month. It passes functions into functions. It has data types for my client accounts and for time entries and for the various operations that can be run. It doesn’t do any fancy M word stuff. It’s all in one file. It’s not pretty, and I don’t think it has great discipline about boundaries yet. But it compiles, and when I want to change something, the compiler helps. My white whale since I was 19, a useful program in Haskell, finally harpooned.
What fascinated me about Haskell when I was still a teenager? Who knows. I had been coding with increasing enthusiasm since I was 10 or 11 but I was no wunderkind. I certainly hadn’t attained anything like the skill or, more importantly, taste I had after just a few years in the working world. What I like about it today is that it’s quite natural to program in Haskell by building a declarative model of your domain data, writing pure functions over that data, and interacting with the real world at the program’s boundaries. That’s my favorite way to work, Haskell or not.