It’s interesting that at Bellroy, we lean on very few of these things, but are big fans of Haskell. Single binary distribution is probably the only one, but because of AWS Lambda we had to use musl libc, which (eventually) pulled us into the Nix universe. That would be a big ask for a new potential Haskell user, and we didn’t go zero-to-Haskell in one jump.
The thing that sold us on strongly-typed ML-family functional programming was a really successful experiment with Elm, and then asking “how can we get more of this?”. But these days, Elm is not as exciting as it was (perception of stagnant core language, etc) and I don’t think we can rely on it as an on-ramp for Haskell as much as before. Purescript also seems pretty quiet, too, and Haskell frontend is halfway through the transition from GHCjs to native JS support. Once that finishes, I think offering relief from the usual JS-based frontend world will be a great pitch.
If I were to ask my colleagues what they thought were the most valuable things we get from Haskell, I think most of the answers would fall under a few broad themes:
- Enforced consistency: Just
newtype
-ing the zillion different record ID types in a business eliminates so many bugs. Other fundamental Haskell features like exhaustiveness checking incase
expressions also have extremely good power-to-weight ratio. But unlike the “simple Haskell” advocates, we’re quite happy to take on something like theservant
library family. Getting enforced consistency between the API definition, the generated OpenAPI documentation, the implementation of the endpoints and the API clients is worth learning the typelevel features that it requires. - Local reasoning: It’s much easier (compared to other languages) to know what a piece of code needs and does just by looking at it, without having to keep global state in our heads. The classic split between pure and
IO
-using code is a big part of this, but also the fact that the type signatures track what data is in a “reader”. - High average library quality: If there’s a library on Hackage, and the library is nontrivial, there’s a good chance that it is solid. This is partially because library authors also benefit from №1 and №2 above, but because the use of advanced features tend not to leak across library boundaries.
Some comments on language complexity and “advanced” libraries:
- We are very careful to set the upper bound of the Haskell features in common use in our codebase. (Roughly: GADTs are OK where genuinely required, you might see the odd Rank-2 function type, but the majority of the code is fairly simple.
servant
is probably the only library doing serious type-level stuff.) - On
lens
: We make heavy use of “basic” lens (nested record access and update, injecting errors into prisms, extracting and summarising data from complex structures). This did mean helping all of Tech Team through Optics By Example, but has paid off handsomely. Occasionally we need to do more advanced things, but they’re rare and heavily commented. (One example: usingControl.Lens.Plated
to walk and rewrite a complex syntax tree.) Contra @BurningWitness, we almost never use lenses to wrangle “state blobs”, but still find them incredibly handy. - On effect systems: dynamic dispatch is extremely attractive to us, primarily because of the dream of effortlessly swapping in test versions of effects where the real version would force us to interact with remote services. At the moment, we use “handles” of the form
data FooHandle m = FooHandle { action1 :: Arg -> Arg -> Arg -> m Results, action2 :: ... }
, but may eventually refactor towards an effect system library.