Haskell: `GHC2021` and language extensions in 2022
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.
EmptyCase
:\case {}
EmptyDataDecls
:data Void
BangPatterns
: programmer strictness annotations
These too, but they’re less integral.
TupleSections
: more tuple syntax (which I personally don’t use)ImportQualifiedPost
: cleaner import syntax
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.
MultiParamTypeClasses
: or noMonadReader r m
for you!FlexibleInstances
,FlexibleContexts
: relax silly rules
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 withDefaultSignatures
to provide a “default” instance) - via
DerivingVia
, generate instance through a coercible type which already has an instance (to use withnewtype
s) - via standard mechanisms built into the compiler for certain type classes:
Eq
,Ord
,Functor
withDeriveFunctor
, …
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
:
GADTs
: surely we’re used to these by now!RoleAnnotations
: extra syntax required for some libraries, safeDefaultSignatures
: type class extension, very often handy
And the ones I appreciate are personal decisions:
TypeFamilies
: I love ‘em, but they’re still relatively newDataKinds
: lovely but the syntax is not self-explanatoryMagicHash
: for theGHC.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.
OverloadedStrings
: can force extra type annotations, prefer per-moduleFunctionalDependencies
: remains useful, though associated types viaTypeFamilies
often work betterPatternSynonyms
: amazing but huge, and without enabling you can still use (just not define), so per-module is sensible
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)