For a language that's beloved for its ability to guide the structure of programs into being easier to understand, easier to maintain, and easier to get correct, there's not a lot of resources on how to best use the tools that Haskell provides. Lots of terms and buzzwords, not a lot of in-depth practical guidance on best practices. So I've put together a list of community blog posts and articles on the theme of building more correct programs.
These are split roughly in two groups: posts about how best to leverage the type system to eliminate errors before they even occur, while striking a balance between type complexity and usability; and posts about using Haskell's best-in-class testing facilities to shore up what can't be typed.1 Despite the hype around Haskell's type system, it's unlikely in a program of any complexity that you'll be able to completely eliminate the possibility of bugs and errors purely through the type system alone. Using both types and tests gives you a better power-to-weight ratio for building maintainable, bug-free programs than either does alone.
Note that I'm explicitly not including articles and resources about basics or setup of the topics in question. Instead of "what is a
Maybe and why use it," think "what are some typical patterns around using
Maybes in a real codebase." Instead of "what is property-based testing," think "here are best practices around choosing properties to test." Since there are already plenty of good introductory resources on things like "how do I get started with QuickCheck," we'll focus here instead on how best to use the tools we have.
Michael Oswald has compiled this post and all the linked articles into a single document, which you can view here. Thanks Michael!
Posts in each section are roughly ordered by difficulty.
Posts on designing and structuring code
Essential reading. A very typical design technique of restricting what inputs your functions take to ensure that they can't fail.
The most typical instances of using this are
NonEmptyfor lists with at least one element and
Naturalfor non-negative integers. Oddly, I don't often see people do the same thing for other structures where this would be useful; for instance, nonempty
Vectors or nonempty
Text. Thankfully, it's easy enough to define yourself.
data NonEmptyVec a = NonEmptyVec a (Vector a) -- An additional invariant you might want to enforce is -- 'not all whitespace' data NonEmptyText = NonEmptyText Char Text
Algebraic blindness, by David Luposchainsky
The motivating problem: if I have a
Maybe Int, I only implicitly know what the two branches mean.
Nothingcould mean "something bad happened, abort," or it could mean "I haven't found anything yet, keep going." Conversely, a value of
Just xcould be a useful value, or it could be a subroutine signalling that an error code occurred. The names are too generic; the structure only tells us what we have, not what it's for.
Goes through how we might solve this problem by defining new datatypes that are isomorphic to, say,
Bool, but with more useful names. Note that the problem that the article talks about with regards to not having typeclass instances for your custom types can (since GHC 8.6) be solved using
Parse, don't validate, by Alexis King
Once all your functions have their inputs suitably restricted, how to actually go about ingesting real-world data (which will always have issues) and producing the types we need? Enforce it at the 'barriers' in your code. That might be at the very start of an HTTP handler, it might be between two modules in your codebase. Do it right and your core logic contains only what it needs to solve the actual problem; no need for cluttering it up with extraneous error handling.
On Ad-hoc Datatypes, by Jasper Van der Jeugt
In a similar vein to preventing algebraic blindness, make your code more readable by naming things with datatypes, even ones that only live in a single module. Defining new dataypes is cheap and easy, so do it!
A worked example of going from a functional, if difficult-to-maintain piece of code, to a design where all the invariants are type-checked and there's no potential crashes to be found.
One thing that you should take away from this article is just how much of the refactoring is completely mechanical and required no understanding of the code at all. This is the biggest thing that people are talking about when they say that Haskell is "easy to maintain": it's possible to make sweeping changes to your code and improve the design with almost 100% certainty that it will still work without needing to think about what the code is doing at all.
EDIT 2020-01-22 (Thanks Michael Oswald!):
While there are fancy ways of injecting things like DB access and side effects into your application, such as monad transformers or effect algebras, the dead-simplest way to avoid turning your program into IO-soup is to just pass around records of functions. While there's nothing wrong with just leaving things in IO when bootstrapping a new project, if you find yourself needing to abstract over your concrete effects, try reaching for this before something more complicated.
Weakly Typed Haskell, by Michael Snoyman
A short example of preventing errors by restricting our function inputs. Uses the streaming library
conduitfor its example, but should be understandable without knowing too much about it.
The Trouble with Typed Errors, also by Matt Parsons
Or: "Avoiding a monolithic error type"
Good, but not necessary when starting out. While the approach described here is pretty cool, it's also somewhat heavyweight. You'll have to decide for yourself whether it's worth the extra cognitive overhead. Truthfully it can often be quite reasonable to have a single, monolithic sum type for all your application/library errors.
Type-Directed Code Generation, by Sandy Maguire.
This post is entirely reasonable to skip, as it requires being familiar with some of GHC's esoteric type extensions. Still, it's a cool introduction to using the more powerful features of the language to make interacting with a complex protocol (gRPC) less error-prone.
Posts on testing
Practical testing in Haskell, by Jasper Van der Jeugt
A short post about writing property tests for an LRU cache. Main takeaway is what Jasper terms the 'Action trick': generating complicated data more easily by instead generating a sequence of events that could happen to the data and constructing a value accordingly. For instance, you could generate a binary search tree by generating a sequence of insertions and deletions.
Property-Based Testing in a Screencast Editor, by Oskar Wickström
Probably the best series of posts I've read about using property-based testing in practice. Lots of interesting tricks and techniques explored. Highly recommended.
Choosing properties for property-based testing, by Scott Wlaschin
More of a general post about PBT. Provides a way to mechanically come up with useful properties for your code. While you should still think through other, domain-specific invariants your program should have and test those, Scott provides some common starting points.
Finding Property Tests, by Hillel Wayne
More starting points for figuring out what properties to test.
Using types to unit-test in Haskell, by Alexis King
Introduces a way of doing more "traditional" unit testing for code with side effects, using mocks and hand-written tests.
Time travelling and Fixing Bugs with Property-Based Testing, by Oskar Wickström
Another large-ish case study of using property-based testing, this time introducing a technique to help ensure that you're genuinely testing enough of your code's input space.
Metamorphic Testing, by Hillel Wayne
Another more general PBT post. The motivating problem: vanilla PBT assumes it's easy to generate inputs to our code. Sometimes it's not. For instance, what if you're testing an image classifier neural net? You can't randomly generate images, because you don't know what the output classification should be for a random image. So we might only have a small set of manually-classified test inputs. Metamorphic testing is a way of expanding our set of test inputs programmatically by transforming the inputs we do have in some way and finding relationships between the original result and the transformed result. For instance, if we invert the color of one of our test images, our classifier should probably give us the same result. If you make a property out of that, you now have more test cases for free, and that catches more bugs.
Unit testing effectful Haskell with monad-mock, by Alexis King
Introduces a way of doing more "traditional" unit testing, but focused more on doing white-box testing, checking whether the code under test performed certain operations, rather than just expecting on the output. Since this is Haskell, doing that is a little bit more unusual.
Personally, I'm not a fan of writing tests like this because they feel like they couple the tests to the implementation too tightly, and calcify design decisions too quickly. Still, if there's something that's legitimately too difficult to sandbox for your testing environment, it's a useful pattern to be aware of.
I'm fully aware that this is not a complete listing even of topics that Haskell programmers know about and regularly make use of.2 If you feel there are articles missing from here that are clear, easy-to-understand, and go in-depth on how to use correctness-enforcing techniques, please let me know!
Found this useful? Still have questions? Talk to me!
Before you close that tab...
Want to write practical, production-ready Haskell? Tired of broken libraries, barebones documentation, and endless type-theory papers only a postdoc could understand? I want to help. Subscribe below and you'll get useful techniques for writing real, useful programs straight in your inbox.
Absolutely no spam, ever. I respect your email privacy. Unsubscribe anytime.
↥1 Or at least, not typed easily.
↥2 Tagged datatypes, for instance. It’s easy to explain what they are, but I haven’t seen good examples of how people use them in a real codebase.