My thoughts on Haskell in 2020
I gave a talk in October 2019, called Stick to Simple Haskell. I put a lot of effort into writing and preparing it. Quite a few people came up to me to say they really liked it. They weren't your usual suspects: those that will never give up type level programming. Rather, they were normal Software developers. Like me and many others, they want their day job to be a bit less shit. I think some of the ideas in that talk are worth spreading, or at least worth talking about. Initiatives like the Boring Haskell Manifesto are a breath of fresh air and we need more of those. This is my tiny contribution.
30 Years old
In 2020, Haskell will turn 30. Since the beginning, the language designers wanted Haskell to be used for:
- teaching functional programming
- innovating and advancing programming language research
- building real world applications and large systems
Let's ignore teaching and focus on the other two.
Programming Language researchers love Haskell because GHC can be augmented through Extensions. GHC is the perfect playground to try out new ideas. It's pretty common nowadays to open up a file and be greeted with a wall of extensions:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
These radically change the language and the type system. You get to work with a much more complicated and capable beast. You get closer to Dependent Types. You get more guarantees at compile time because more information can be encoded in types. We usually refer to these as Fancy types.
And yet, "Haskell the language" hasn't evolved much since 1998. In very broad and inaccurate terms, Haskell98 is what you get when running GHC with no extensions enabled. You get simple data types, type classes and a lazy purely functional core.
Building applications in Haskell
PL researchers go all in on Fancy types. But I'm no PL researcher. I'm a Software developer. Recall that one of the designers' goals was for Haskell to be used in the Industry, to write real world applications. Is Fancy Haskell suitable for this?
Looks good on Paper
Writing production software is very different than writing a paper.
Once a research write up is out, it doesn't have to be maintained. Techniques and tricks shown in code samples that look good on a PDF aren't indicative of how they would affect a real codebase. Researchers can rely on experimental features and untested extensions. They can get by with slow compilation times.
Inclusivity
Writing production software is a team effort.
I want to work with a diverse group of people. I don't want a PhD to be a requirement to work with Haskell. I want a codebase that is accessible to all skill levels. Most business logic in applications isn't rocket science. There's no reason to make apps more complex than they need to be.
Selfishly, as a company betting on Haskell, you get access to a wider pool of talent by lowering the barrier to entry.
Marginal benefits
Fancy types give us more guarantees, so we can write safer and more correct code. Why would we want to give that up?
I'm not convinced the benefits are that dramatic. The jump from a mainstream programming language to Haskell is fairly stark. There are clear gains in having a compiler help you write correct code. That might still not be the most correct code. It's true, with Fancy types I could gain that extra x% confidence. Is the complexity worth it?
In the interest of being pragmatic, we need to look out of the box for a second. We need to realize types are an invaluable tool, but they're not the only tool we can employ. Types aren't an excuse to avoid testing.
Simple Haskell
I'm not suggesting we ignore everything that happened after 1998.
Haskell98 is a nice foundation, but we can improve it. The core idea is to accept the type system as it is and focus on ergonomics. Make the language nicer, not the type system.
This boils down to using Generics and enabling ergonomic extensions.
Generics aren't available in Haskell98. They're fantastic to remove boilerplate. You can automatically derive JSON instances and lenses with generic-lens.
Certain extensions are unharmful and make Haskell that much nicer to work with.
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
Application architecture
Most applications should just be built as:
app :: Env -> IO ()
where Env
represents a dependency injection container.
data Env = Env
{ usersCache :: TVar [User]
, postgresConnection :: PG.Connection
, log :: Severity -> Text -> IO ()
, fetchUser :: UserId -> IO (Maybe User)
, storeFile :: Filename -> ByteString -> IO (Either Text ())
}
My euristic to determine if something belongs to Env
:
- Is the thing a resource shared across the application? (ie. a Postgres connection)
- Do I want to provide a different implementation? (ie. a mock during testing)
When the type gets big, you can split it up further.
data UserService
= UserService
{ fetchUser :: UserId -> IO (Maybe User)
, updateUser :: User -> IO (Either UserServiceError ())
, deleteUser :: UserId -> IO (Either UserServiceError ())
}
data Env = Env
{ userService :: UserService
, ...
}
When the application gets big, you can refactor to use the ReaderT design pattern.
Haskell in 2020
Haskell managed to succeed despite trying to please two different crowds: Programming Language researchers and Software developers. 30 years later, we have a language that is still used by both groups. But their needs couldn't be more different.
As Software developers, we need to understand which Haskell features are useful in writing applications. And more importantly, which features are harmful or ineffective. PL researchers and Software developers share the same tool, but that's not an excuse to blindly adopt Fancy types.
As Haskell developers, we need to realize not all problems deserve to be turned into a paper. You have a choice:
- Embark on a 4 hours quest to find a beautiful solution at the type level.
- Spend 10 minutes and solve it the boring way. Maybe even write a test.
It's ok to sacrifice a bit of type safety in favour of Inclusivity. We can still make sure our software is correct through testing. This is what Simple Haskell really is about. The term Boring Haskell works even better.
Writing Simple/Boring Haskell is a joy. Concrete code is so liberating. Abstractions are nice, but Boring Haskell is nicer.
Next time a beginner talks to you:
- Don't recommend Servant. Scotty works great. Type safe routing can wait.
- Don't recommend any effect system. Massive abstractions can wait.
- Don't trash Elm. Or Go. You're not accomplishing anything.
- Don't recommend Nix. Stack works great. Yes, no matter how many hours it took you to get nix-build do what you intended.
- Continue talking to them, even if they don't have a PhD.
In 2020, get off the Ivory tower. Write Boring Haskell.
In an unexpected turn of events, just a few hours after this post came out, Matt Parsons published a very good article promoting Junior Code. If you liked this post, you should give it a read. If you didn't like this post, then it's required reading.