Back in 2018, Alexis King wrote an incredibly handy blog post titled An opinionated guide to Haskell in 2018. The section on language extensions especially is detailed and discusses rationales for sets of extensions. A year ago I started writing lots of Haskell, and used her suggestions as a basis for my defaults. Since I tend to write Cursed and Bad Code, I ended up bloating my default extensions to a recent maximum of 32.

As we come to the end of 2022, thanks to a recent stack update, the GHC2021 language set is finally convenient for all to use. Here I comment on how and why you should use it, and some other handy extensions to know about.

Background

The Haskell language is de facto defined by whatever the most popular compiler GHC permits. However, it’s also a standard, which GHC continues to respect. The extensive research and community effort poured into GHC over the decades have resulted in better abstractions, extended syntax and adjusted language semantics. Many of these features are locked behind language extensions, enabled either per-file or globally for a project.

There are a lot of these extensions. Some are deprecated (DatatypeContexts), others implied by common extensions (ExplicitForAll) others considered unusual style and rarely seen (ImplicitParams). But some enable seemingly rather basic features, like MultiParamTypeClasses and GADTs. Perhaps you use DataKinds everywhere and are tired of typing out the LANGUAGE pragma. (I do, I am.)

Default extension lists enable stating with clarity “this is the Haskell flavour we’re using”. But really, most people are using the same base with a few extras swapping in and out for taste. The GHC developers recognized this, and in GHC 9.2 introduced a “language set” GHC2021 comprising a good chunk of the common safe extensions. Now my default set is down to just 10!

The GHC2021 language set

Using GHC2021

If you’re using Cabal, this has been easy for a long time. In your library or test-suite stanza or wherever, add default-language: GHC2021. For example:

library
  exposed-modules:
      # ...
  # ...
  default-language: GHC2021

Stack users can now benefit from this in a similar way. To use GHC2021 as the default language set for every part of your package (library, executable, tests), add language: GHC2021 to the top level of your package.yaml. Example:

name: my-package
# ...
language: GHC2021

What GHC2021 means for your project

The user’s guide page documents which extensions the flag enables. Allow me to comment briefly on what it all means for semantics and permitted syntax. I’ve selected the extensions I feel are most important, and grouped them according to their role. For a full list of extensions enabled by GHC2021, see the user’s guide page.

Adds missing syntax

The Haskell standard omits some important syntax. This stuff should be in Haskell, end of.

These too, but they’re less integral.

Makes type classes a bit more sensible

Type classes have evolved a lot over GHC’s lifetime. GHC2021 thankfully enables the ones you need to have a good time.

Fleshes out kind system

Through extensive changes to Haskell’s kind system, GHC gives programmers extremely powerful tools. Most of the big players are provided by GHC2021. The syntax additions are small/ingrained enough into the existing language that reading them shouldn’t cause much distress to unaccustomed programmers.

Powers up type system

These are larger changes to the language which are likely to surprise new users. They are focused around the explicit quantification and scoping of type variables, and the forall construct. TypeApplications in particular adds an extra dimension to function calls.

Extends deriving mechanism

GHC can derive efficient type class instances for many type classes. GHC2021 enables them all. They do nothing if you don’t request GHC derive such an instance. The user’s guide page Deriving instances of extra classes (Data, etc.) and its sibling pages are useful for further reading.

StandaloneDeriving is also enabled, which provides more flexible syntax for deriving weirder types.

Other extensions I always use

DerivingStrategies

An essential instance derivation extension that barely missed out on GHC2021 inclusion. GHC now has a few ways to derive instances:

  • via DeriveAnyClass, generate instance with no explicit definitions (useful with DefaultSignatures to provide a “default” instance)
  • via DerivingVia, generate instance through a coercible type which already has an instance (to use with newtypes)
  • via standard mechanisms built into the compiler for certain type classes: Eq, Ord, Functor with DeriveFunctor, …

The compiler decides on the method to use. The heuristic is pretty straightforward… but also totally hidden from the user. And judging from recent innovation, the deriving mechanism may well continue to evolve.

DerivingStrategies permits the user to specify which derivation method to use:

data Tree a = Node a (Tree a) (Tree a) | Leaf
    deriving stock (Eq, Ord)

newtype BTree a = BTree (Tree a)
    deriving (Eq, Ord) via (Tree a)

Explicitness is always good. I make a point of using deriving stock everywhere.

DerivingVia

Mentioned above, this is another deriving extension. It generalizes GeneralizedNewtypeDeriving (lol), which is another way of saying you should always use DerivingVia in its place. See the documentation for details.

DerivingVia actually implies DerivingStrategies, but while both remain uncommon to see in Haskell code, I like to point them out separately.

LambdaCase

How’d this slip through the cracks? It adds a key piece of syntax:

intToBool :: Int -> Bool
intToBool = \case 1 -> True
                  _ -> False

The only pointfree style that you can’t disagree with. Keep him in your hearts, and your default-extensions.

NoStarIsType

Haskell originally used * to denote the kind of a type, like Int :: *. As the GHC folks began to muddy the line between terms and types, it’s gotten painful to support this syntax decision. Behind the scenes, the kind of a type has been Type for a while… but a default extension StarIsType re-enables the old syntax for compatibility. So folks who like to multiply type-level naturals have to fumble around with imports and language extensions.

In a perfect world, extensions like this would not exist. But this is not a perfect world. Take comfort in the knowledge that some day, NoStarIsType will be the default. Though, um, one of the GHC proposals concerning it gives a 7-year deprecation schedule. Oh.

Larger jumps

I like to enable these extensions by default, but I understand why they might be controversial. Firstly, ones I hope to see in the next GHC202X:

And the ones I appreciate are personal decisions:

  • TypeFamilies: I love ‘em, but they’re still relatively new
  • DataKinds: lovely but the syntax is not self-explanatory
  • MagicHash: for the GHC.Exts enjoyers out there – honestly not a big jump from tick prefix notation

Extensions to enable on-demand

UndecidableInstances

The idea of embracing undecidability tends to put people off, but don’t be afraid of her! This is required for much type class wizardry, in the cases where GHC’s heuristics can’t guarantee instance resolution will terminate. It doesn’t impact runtime behaviour, but those restrictions keep you on the straight and narrow, writing sensible code.

Enable only for files where compilation fails and GHC says you need UndecidableInstances and you were expecting that to happen.

AllowAmbiguousTypes

This often goes hand in hand with TypeApplications-heavy code. It indicates a distinct function design, that certain functions can’t be used without an explicit type application, which is unusual to most Haskell developers.

Similar to UndecidableInstances, enable this only when GHC tells you to.

StarIsType

As described above, if you enable NoStarIsType by default, you may need this on occasion to work with modules using the old Int :: * style.

ApplicativeDo

A fantastic extension that shines in the infrequent, yet surprisingly real-world cases where you have some f :: Type -> Type which has an Applicative instance, but no lawful Monad instance.

According to Alexis King’s 2018 post, it won’t always work, might mess with “buggy” Applicative/Monad instances that don’t follow best practices (which should be mostly resolved by the Monad of no return proposal, in the… far future), and hides a hefty amount of implementation complexity. I’ve only used it recently, and went with per-module. Up to you.

CPP, TemplateHaskell

These are massive, scary extensions that do a lot, and you should only enable one when you have a specific need for it.

Special mentions

These extensions I don’t personally use as defaults, but are useful or worth considering.

Wrap-up

GHC2021 is good to use. It communicates to fellow authors some base expectations they should have about the project’s syntax. I hope this post may illuminate some of more important additions to know about.

Acknowledgements & further reading

Thanks to all the Haskell folks who freely dispense their experience and knowledge: Alexis King, the Kowainik team, everyone on the #haskell IRC channel on Libera Chat, among others.

Some related links for further reading:

  • https://kowainik.github.io/posts/extensions (May 2020)
  • https://limperg.de/ghc-extensions/ GHC 8.6 (Oct 2018)