Delivering with Haskell

14 min read Original article ↗

fommil

This is my advice to teams that are interested in using Haskell, for the first time, to ship a project. My advice is based on two years of Haskell experience, combined with war stories from friends.

Press enter or click to view image in full size

Unfortunately, I am hearing more and more anecdotal evidence that Haskell projects have a higher chance of failure than other languages. By finding patterns in the projects that failed, and comparing to those that succeeded, I’d like to help steer new teams away from danger. The TL;DR is you’ve already spent your entire novelty budget by using Haskell; so don’t do anything fancy with the type system.

I’m not going to preach about why anybody should use Haskell, I’m not a language evangelist, although I do believe it is the best general purpose programming language for shared codebases and has best-in-class developer tooling. If you have found a great use for Haskell, and have convinced management to let you run with it, then hopefully you’ll learn something from my advice.

I’ll also assume that there is at least one person on the team with some Haskell experience (commercial or hobby) and that all the core members of the team have opted in. Trying to start a project in a language with inexperienced and / or reluctant team members (heckling every time something doesn’t work) is guaranteed to fail, no matter what language you choose.

Education

Even if the core team have all used Haskell before, there will be peripheral people who will be finding excuses to complain about how your project is inaccessible. Make sure you have a light on-boarding process for them to dip in and out, and always be available to pair and help at the drop of a hat

Make sure your README links to great learning material. I recommend Graham Hutton’s book “Programming in Haskell”, and recommend against both “Learn You a Haskell” (it lacks relevance) and “Haskell Book” (it is aimed at non-programmers, which is not you).

Haskell projects have failed because the core team disappear off into a silo and leave everybody else behind. It is perceived as elitist and is a common complaint against Haskell adoption. Learn from past mistakes and remain approachable and eager to teach and share.

Tooling

Most new Haskell developers are using VSCode as their text editor, which has lots of relevant extensions. For small projects the Haskell Language Server works great. For the Emacs and Vim fanatics, they will find a mature set of idiomatic plugins. I’ve personally been using an experimental Emacs mode called haskell-tng, which provides a lot of IDE features, and it’s been a real pleasure to use.

There are two main build tools for Haskell. I wrote about them two years ago: they are compatible and team members can choose the one they prefer if you use stack.yaml as the source of truth and stack2cabal to produce cabal files that are committed to the repo. However I recommend using cabal in CI as I’ve found it, and the servers it talks to, to be more actively maintained, reliable, easier to install, and easier to cache.

Do not use Nix. Nobody understands Nix unless they have time to burn, Nix barely ever works, the guy who uses NixOS can never get anything to work, Nix will kill your project. Let the NixOS guy keep the nix files up to date for his own benefit, if he really must, but never make it sound like a requirement.

Make sure to document how to setup a text editor and build tool in your README for all the platforms used at your work so that nobody can claim that they don’t know where to begin. Start with the Haskell installer tool and make sure to document Hoogle very prominently, because this is a real gem and a huge productivity booster.

Haskell has a myriad of linters and formatters. I recommend adding ormolu as a build-tool-depends (i.e. managed by the build tool, no user setup required) and enforced in CI. It will stop many bikeshed discussions from the outset of the project. That’s important because the rest of your company is watching you and it will look bad if you waste time talking about whitespace: it perpetuates the myth that Haskellers dwell in the Ivory Tower. Just pick a tool, let it make the decisions for you, and focus on shipping.

Language Extensions

One of the biggest cultural shocks of Haskell is that nobody is actually writing Haskell; they are almost certainly writing Haskell plus a selection of language extensions.

There are over 120 language extensions, and many of them require the reader to understand a research paper. I think it is amazing that there is this level of documentation and consideration behind every extension, and I also think it is proof that the Glasgow Haskell Compiler has succeeded as a vehicle for academic research. However, as an industrial developer, I am aware that every language extension brings with it an educational and conceptual overhead to every reader and maintainer of the code, varying levels of tooling support, non-obvious feature interactions, and confusing compiler error messages. I previously wrote about some of these issues in Simple Haskell is Best Haskell .

For teams who are adopting Haskell for the first time, my advice is much stricter than those of the Simple Haskell initiative. The Haskell ecoystem is like a loaded gun without a safety clip; stick to stock Haskell, as defined in the 1998 report (updated in 2010), and you’ll be fine. Beyond that, I think it is essential to have a process for opting-in to specific language extensions, and also including the periphery members of the team on the selection panel to keep the list grounded.

I propose that only the following language extensions should be considered for acceptance by your panel for at least the first year. Once you deliver, and if everybody agrees that Haskell is officially adopted at your company, then you can consider opening it up further. Notably, all of the following extensions are fully supported by all the tooling I am aware of.

  • ScopedTypeVariables, ExplicitForAll, InstanceSigs, KindSignatures - these make it possible to add type signatures in places where Haskell2010 overlooked the need to do so, producing better error messages and self documentation.
  • LambdaCase - is purely syntactic and saves repetitive typing.
  • NamedFieldPuns - is an explicit way of interacting with record fields, and saves repetitive typing.
  • OverloadedStrings - which aligns text entry with number entry and makes it easier to use alternatives to the built-in String type.
  • BangPatterns - allows specific values to be evaluated to normal form and is necessary to avoid the occassional memory leak, or to optimise performance. Not to be confused with NFData, which is used to force evaluation.
  • ViewPatterns - is a powerful way to pattern guard, and hence saves repetitive typing.
  • TupleSections - “do what I mean” syntactic sugar when dealing with tuples, saves repetitive typing.
  • GeneralizedNewtypeDeriving - “do what I mean” syntactic sugar for generating instances for newtype things.

And an honorary mention goes to

  • CPP - allows the use of the C Preprocessor and is absolutely necessary for dealing with multiple versions of third party libraries or interacting with native code across platforms.

It is only an honorary mention because CPP is not supported by the ormolu code formatter. The authors of ormolu believe CPP is harmful and frankly I disagree with them. Languages that don’t support cpp end up sorely missing it and nobody has ever proposed a backwards-compatible alternative.

Get fommil’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Haskell veterans will be asking “that’s all?!?” at this point. And, no, I didn’t forget about RecordWildcard or RecordDotSyntax (they implicitly bring magic symbols into scope), Derive* and Deriving* (they encourage the use of fancy types), DuplicateRecordFields (sum types shouldn’t have fields since it produces partial functions), NoImplicitPrelude (custom preludes are hype), TypeFamilies, GADTs, or anything to do with dependent typing (extremely fancy types). If you feel you’re reaching for TypeApplications it’s a good sign that you’re doing something that is too fancy, because it means inference broke and you need to tell the compiler what to do.

I also advocate against some traditionally “safe” extensions such as FunctionalDependencies and MultiParamTypeClasses, which are somewhat harmless on their own but are the gateway drug to even more extensions such as FlexibleContexts, OverlappingInstances and IncoherentInstances. Their main usecase is to enable the creation of MonadReader and MonadState, which are not as useful as one might expect (you’ll end up in the MultiReader rabbit hole) and can anyways be replaced by coding to explicit ExceptT and StateT.

Incidentally, I consider -fno-warn-orphans to be a language extension, and one that I passionately discourage. I believe it attacks the very fundamentals of the typeclass mechanism. Use a newtype, or fork either the package containing the class or the data type into your codebase (Haskell build tooling makes this very easy), you can be a good citizen by giving that back to the community behind a flag so that people can opt-in to get the instance. If there are conflicting orphan instances in your codebase it will break your ability to reason about what is being used in each test and will expose you to rare (but hugely time consuming) incremental compilation bugs. Related to this: use QuickCheck but don’t use Arbitrary. Instead, write explicit Gen generators for everything and choose what you want to use on a per-test basis. It’ll be a PITA to do this initially, but you’ll thank me in the long run when you start writing more complex tests.

No Fancy Types

If you sold your management on dependent typing as a way to reduce defect rates, then I don’t think Haskell is the right tool; use Idris or Agda instead.

The Haskell type system has been pushed in directions such as type families and generic / type level programming. I think every Haskell developer should read Thinking with Types to understand what is possible, but also refrain from doing anything like that in a codebase that is trying to win the adoption of Haskell.

A common theme in every single failed Haskell project that I am aware of is that they tried to do too much with the type system and the code became unmaintainable and the error messages impossible to reason about. I am aware of some projects that have succeeded while making heavy use of TypeFamilies and generic programming, but I note that those teams were specifically set up as advanced Haskell teams and only hired people with more than 2 years of commercial experience. So there is value in fancy types, but it’s just a lot further down the road than your first project.

Avoid “effect systems”. There has been a craze in recent years to try and get the Haskell type system to track individual interactions with the real world at the type level rather than letting everything be consumed in a single IO monad. I don’t personally understand why people want to track this at the type level, and the effect systems that have popped up to solve the problem have a huge impact on the architecture of a program and that’s quite a dangerous move to take with an experimental system. I’ve been involved in the unravelling of a project that used an effect system and I think it is fair to say that the project would have failed if we had not done that, and also that the cost to unravel was extremely high; not every project has the luxury to make that kind of expensive mistake. It’s also worth noting that effect systems have poor performance.

Just use mtl style, or better yet, don’t use a typeclass encoding (you might never need to define a class) and just use Records of Functions. I wrote about this in detail in Local Capabilities with MTL and Some Limits of MTL with Records of Functions.

A lot of libraries assume that you will use the DeriveGeneric language extension and then use generic programming along with record syntax to create serialisers and deserialisers. This is somewhat like using a missile to destroy a fly, since the layers of indirection are numerous and although it works for simple examples, it turns into a rabbit hole when you have something that doesn’t fit the mould. Rather than doing any of that, I recommend just writing the boilerplate by hand, or better yet… automating the generation of the boilerplate with a tool (appropriately) named boilerplate, which sweeps away the justification for a lot of fancy types by noting that it is “like the way IDEs do it for languages such as Java, C++ and Go”. I am coming to the conclusion that the entire thesis of “scrap your boilerplate” is actually wrong, and it’s solving a non-problem.

Avoiding fancy types has some implications for the libraries that you might want to use. For example, the popular webframework Servant will require users to enable several language extensions, using TypeFamilies under the hood. The impact on users is that error messages are incomprehensible. I recommend going with something more basic, even at the cost of writing more boilerplate, such as Scotty. I have seen a project fail (partly) because it was using Servant and peripheral developers were unable to maintain the HTTP endpoint definitions for their (non-Haskell) services. That project also went for a long time without Swagger definitions and consumers complained that they couldn’t understand the HTTP API exposed by the Haskell services. Don’t forget to use a common language at the boundaries of your system.

Somewhat related to “fancy types” is general advice to avoid the bleeding edge of anything. If there’s a new library claiming to do something better than a library that has been around for over a decade, maybe just stick with the more conservative older library. Remember, you already spent your novelty budget by choosing Haskell.

Refactoring

Haskell makes it really easy to refactor code. Too easy. It’s a bug, not a feature.

Press enter or click to view image in full size

https://twitter.com/rickasaurus/status/1313471620351229958/

The only way to fight excessive refactoring is good processes and strong leadership. Every refactor to a codebase incurs a cost in terms of disruption to in-flight work, and a conceptual overhead for other developers who must review the code and re-understand the code they were once familiar with. The payoff is maybe a more maintainable codebase in the long run. Haskell makes it much easier to have confidence that the refactor is correct, but do not forget the real human and project management cost, as well as the bugs that the type system can’t catch and may be compounded by fancy types that automatically generate things like serialisers.

Emily offers us some sage wisdom on this topic:

Ask “Is my code necessary, or is it a convenient refactoring that makes more sense in terms of my personal understanding of the code?” The answer is rarely “yes”.

— MLE https://twitter.com/pitopos/status/1312098569382359040

General Advice

I’m not aware of any projects that have failed because of the things listed below, it’s more of a general dumping ground for things that I lost time on. Haskell is really stable, but that just means that the bugs are well known and can’t be fixed easily. The biggest gotchas for me where:

  • create a type alias to a single, global, monad stack, and just code against that explicitly instead of using Monad* constraints. For more general code you can use just a Monad constraint.
  • do not using String, it is inefficient and will require you to use language extensions just to implement instances. Use Text instead.
  • do not use exceptions. You think you understand them because you have used them in every other language to great effect, but you do not understand them in lazy languages. If you really really must use them, then use exclusively the *Deep functions from the safe-exceptions package along with lots of NFData instances.
  • invest in writing a really good model based testing framework, at the user request/response level, using QuickCheck to generate user actions with a domain specific shrinker.

And that’s all the advice I have for you! I hope you have fun, and I really hope you can help to increase the adoption of Haskell. I love this language and I think it has missed out due to misconceptions. I hope you succeed, and please let me know either way.