This post was originally published on Medium.
If you’re a Haskell user, you’re probably interested in having the compiler assist you in writing the most correct program possible. The compiler by default enables 18 warnings, and you can enroll in an additional 8 with -Wextra
†, and even more with -Wall
. You’ve probably seen packages compiling with -Wall
, but did you know that -Wall
doesn’t enable all of GHC’s warnings?
The GHC user’s guide lists what it doesn’t enable:
-Wincomplete-uni-patterns
-Wincomplete-record-updates
-Wmonomorphism-restriction
-Wimplicit-prelude
-Wmissing-local-signatures
-Wmissing-exported-signatures
-Wmissing-export-lists
-Wmissing-import-lists
-Wmissing-home-modules
-Widentities
-Wredundant-constraints
-Wpartial-fields
I’ve also experimented and found that it also does not enable:
-Wmissed-specialisations
-Wall-missed-specialisations
-Wunsafe
-Wsafe
-Wtrustworthy-safe
-Wcpp-undef
which aren’t listed in the docs‡; I opened a PR to fix this. The “safe”-related warnings are documented in the Safe Haskell section of the GHC user’s guide.
To truly get every warning, following the pattern of compilers like Clang, you need the -Weverything
flag. If you turn it on, you’ll quickly find out that it gives you more warnings than you want, and you’ll want to turn some off. Here’s a breakdown — quoting liberally from the GHC user’s guide — of what each of the -Weverything
flags does to help you decide which to keep:
Warnings you almost certainly want
-Wincomplete-uni-patterns
Warns you when a pattern match might fail at runtime in a lambda or pattern binding, e.g.:
h = \[] -> 2
Just k = f y
I strongly encourage you to enable this warning, and to avoid writing incomplete pattern matches in general.
I do, however, recommend disabling this warning in tests, where it’s extremely useful for asserting that a function that returns Maybe
or Either
hits the branch you want, while also making its internal value usable. For example:
(Just updatedMetadata) <- runDB $ get metadataId
-- Do something with the metadata
It’s also useful if you make heavy use of the Smart Constructor pattern, and don’t want to write QuasiQuoters that allow you to safely construct them at compile-time.
Again, these exceptions are only meant for test environments, where failing on a random line is desirable. In real code, prefer handling errors (e.g. pattern matching both branches of Maybe
), or preventing them from happening (e.g. using a QuasiQuoter to construct smart-constructed data).
-Wincomplete-record-updates
Enables a warning when a record update could fail at runtime, as in this example from the GHC user guide:
data Foo = Foo { x :: Int }
| Bar
f :: Foo -> Foo
f foo = foo { x = 6 }
I strongly encourage enabling this warning. Also: Because of this error, you should avoid mixing records with algebraic data types like this, which brings us to…
-Wpartial-fields
This warning will trigger when you create a record that could trigger the above warning. GHC 8.4+
-Wmissing-home-modules
This will warn you when GHC compiles a module, but it hasn’t explicitly been passed that module as an argument. This most commonly happens when a module from your exposed-modules
or other-modules
lists references a file not in those lists, causing a confusing error. I believe this warning was created in response to this issue: https://github.com/haskell/cabal/issues/1746
I recommend enabling it to prevent this problem. You should maybe also promote it to -Werror=missing-home-modules
, to avoid confusing compiler errors.
Personally, I find the exposed-modules
and other-modules
lists tedious, frustrating, and error-prone. I recommend using hpack
to automatically create your Cabal file to avoid this kind of error.
GHC 8.2+
-Widentities
In Haskell, you use the function fromIntegral
to convert from any type of integer to another (e.g. Int32
to Int
). toRational
, toInteger
, and realToFrac
serve a similar purpose. This warning will tell you when you’ve used these functions, but haven’t actually changed the type (e.g. Int32
to Int32
)
-Wredundant-constraints
This will warn you when one of your constraints is subsumed by another:
f :: (Eq a, Ord a) => a -> Bool -- Ord implies Eq, so Eq is unnecessary
and when a constraint is unused by the function body:
f :: (Eq a, Ord a) => a -> a -> Bool
f first second = first == second -- The function only uses ==, not functions like > or <, so Ord is unnecessary
-Wcpp-undef
Warns you if a C pre-processor expression is undefined when using #if
, e.g.
#if FOO
It’s rare that Haskell code uses #if
(#ifdef
is more common in my experience), but you might as well turn it on.
-Wmissing-export-lists
This warns you “if you declare a module without declaring an explicit export list”. In most cases, I recommend using explicit export lists, for these reasons:
1. You need an export list to not export “private” functions
2. It allows dead (unreachable) code to be detected
3. It “can ease optimizations like inlining”
Thus I would enable this warning. Note that it’s GHC 8.4.1+.
Warnings you might want
-Wmonomorphism-restriction
Quoting the guide, this warning will “warn/inform you where in your source the Haskell Monomorphism Restriction is applied”. The monomorphism restriction is more than I can explain here, but to take an example from this blog post, this code:
:set -XMonomorphismRestriction
set -Wmonomorphism-restriction
f xs = (len, len) where len = Data.List.genericLength xs
will trigger this warning:
<interactive>:14:25: warning: [-Wmonomorphism-restriction]
• The Monomorphism Restriction applies to the binding for ‘len’
Consider giving a type signature for ‘len’
This is because while genericLength
can return any Num
, without a type signature, GHC will not allow the two types in (len, len)
to be different. It does this because if they were different types, it would have to compute len
twice, which might be unexpected. Only if you give an explicit signature of (Num a, Num b)
will GHC allow the types to be different.
I don’t have very good advice to give on this warning. Many people, like the author of hlint, Neil Mitchell, would prefer to see the restriction removed. It’s even known as “The Dreaded Monomorphism Restriction”.
So potentially you should disable the restriction with NoMonomorphismRestriction
. If you keep it enabled, the monomorphism restriction won’t lead to bugs in your program, but it might lead to confusing behavior. This comment on Reddit gives an example:
a = 34
b :: Int
b = a+5 -- Causes a to be inferred as Int
c = a/7 -- Refuses to compile
In practice, I turned on the monomorphism restriction warnings in our codebase, and found three negligible issues. It was more confusing trying to figure out why I’d be warned about them, when I could tell the code was perfectly sensible. For this reason, I keep the warnings off.
See this guide to GHC extensions for more details.
-Wimplicit-prelude
This warns if the Prelude is implicitly imported. I’m not sure why you would want this — maybe if only part of your codebase uses an alternative Prelude? Our codebase doesn’t turn it on, because we have NoImplicitPrelude
enabled globally, which disables this warning.
-Wmissing-local-signatures
This will warn you when one of your local bindings is polymorphic, and print the inferred type. For example:
ghci> let double a = a + a
*App|
<interactive>:6:5: warning: [-Wmissing-local-signatures]
Polymorphic local binding with no type signature:
double :: forall a. Num a => a -> a
I’m not sure why someone would be concerned about this. I turn this warning off.
-Wmissed-specialisations
Quoting from the GHC guide:
Emits a warning if GHC cannot specialise an overloaded function, usually because the function needs an INLINABLE pragma. Reports when the situation arises during specialisation of an imported function.
This form is intended to catch cases where an imported function that is marked as INLINABLE (presumably to enable specialisation) cannot be specialised as it calls other functions that are themselves not specialised.
(See the next warning for commentary)
-Wall-missed-specialisations
This warning is like -Wmissed-specialisations
, but for all functions, not just imported ones.
I don’t recommend either of these warnings for normal use, because fixing them involves fixing underlying library code. Perhaps if you’re developing a performance critical library they’re a good choice to keep enabled, to prevent performance regressions. Alternatively, you could temporarily enable them when looking to optimize performance.
Warning: While I know that inlining is very important to Haskell performance, I don’t have much experience with it. If someone could comment on their experience using this warning to improve performance, that would be helpful.
Warnings you very likely don’t want
-Wmissing-import-lists
This warning requires all imports to either be explicit or qualified. While I find explicit or qualified imports to be helpful in improving readability, importing everything explicitly or qualified would drive me insane. Especially so for Haskell, where functions like $
and .
feel more like core language syntax than normal functions. I don’t recommend enabling this warning.
-Wunsafe
This warns you that a module is inferred to be unsafe; see the Safe Haskell section of the GHC User’s Guide.
I do not use Safe Haskell, I’ve never heard of anyone using it (though I’m sure they’re out there), and many core libraries (Text, ByteString) are not Safe. I recommend disabling this warning, unless you’re intending to use Safe Haskell.
-Wsafe
Conversely, this warns you that your module is inferred to be safe. This allows you to mark it as explicitly safe, instead of just inferring it.
Again, I would disable this warning.
-Wtrustworthy-safe
This warns if you’ve marked a module as -XTrustworthy
(a social promise that the code is safe), but it could be marked as -XSafe
(a compiler-checked promise that the code is safe).
If you don’t use Safe Haskell, there’s no need to disable this warning, since you won’t be compiling modules with -XTrustworthy
.
-Wmissing-exported-signatures
This GHC guide states that this warning checks that “every exported top-level function/value has a type signature, but not… unexported values.” “This option takes precedence over -Wmissing-signatures
.”
In other words, it disables the warnings you normally get when top-level signatures don’t have a declared type. Top-level signatures are strongly recommended for readability, to constrain your function’s abilities, and as a double check that the function you wrote is what you intended.
I personally consider it a bug that -Weverything
enables this warning, since it makes -Weverything
less restrictive than -Wall
. I filed a Trac ticket here: https://ghc.haskell.org/trac/ghc/ticket/14794#ticket
The copy-pastable list
Based on those explanations, this is the list of warnings I recommend:
ghc-options:
# For details on warnings: https://downloads.haskell.org/~ghc/master/users-guide/using-warnings.html
# This list taken from https://medium.com/mercury-bank/enable-all-the-warnings-a0517bc081c3
# Enable all warnings with -Weverything, then disable the ones we don’t care about
- -Weverything
- -Wno-missing-exported-signatures # missing-exported-signatures turns off the more strict -Wmissing-signatures. See https://ghc.haskell.org/trac/ghc/ticket/14794#ticket
- -Wno-missing-import-lists # Requires explicit imports of _every_ function (e.g. ‘$’); too strict
- -Wno-missed-specialisations # When GHC can’t specialize a polymorphic function. No big deal and requires fixing underlying libraries to solve.
- -Wno-all-missed-specialisations # See missed-specialisations
- -Wno-unsafe # Don’t use Safe Haskell warnings
- -Wno-safe # Don’t use Safe Haskell warnings
- -Wno-missing-local-signatures # Warning for polymorphic local bindings; nothing wrong with those.
- -Wno-monomorphism-restriction # Don’t warn if the monomorphism restriction is used
-Wall
plus warnings vs -Weverything
minus warnings
You can get the same set of warnings compiling with -Wall
plus some added warnings, vs compiling with -Weverything
minus the ones you don't want. Which should you use? For most cases I would recommend -Weverything
and opting-out of which warnings you don't want, because:
- As you upgrade the compiler over the lifetime of your project, you’ll automatically enroll into new warnings. If you were excited to hear about
-Wmissing-export-lists
,-Wpartial-fields
, or-Wmissing-home-modules
(all relatively new warnings) that's good evidence that you'd benefit from an opt-out approach. - You don’t need to know the full extent of
-Wall
's capabilities. If you looked at GHC's user guide to determine what warnings aren't enabled by-Wall
, then picked from those, you'd be missing out on unlisted ones like-Wcpp-undef
.
Counterargument: Some potential reasons to prefer -Wall
plus warnings:
- You want to keep it simple. It’s more complicated than
-Wall
, plus a handful of warnings. This makes it worse if you want a simple list, which I could understand guides like RIO's wanting. It's much easier to explain why a warning is helpful than to explain e.g. why you wouldn't want something called Safe Haskell. - You think users might use a new compiler with your code, and you don’t want them to get scary warnings. For example, if you’re writing a book, then you could plausibly expect readers to use a newer compiler than what you used when you wrote the book. If those users were using
-Weverything
, they might be confused by that your book's sample code generates warnings.
With the caveat that I had to create these objections myself, making them natural strawmen, I think these objections are fairly niche and most people should use -Weverything
.
Disabling warnings on a per-file basis
After you’ve enabled all these warnings, you’re likely to have ones that you want to ignore in only one spot. Unfortunately, unlike compilers like GCC and Clang, you can’t disable warnings for a specific range of code. This is the subject of this (14 year old!) Trac ticket, if you’d like to encourage adding this feature, or want to give a go at implementing it yourself.
// Example of disabling warnings in a block of code using Clang
#pragma clang diagnostic push
#pragma clang diagnostic ignored “-Wshadow-ivar”
// your code
#pragma clang diagnostic pop
You can disable warnings on a per-file basis, though, by adding an OPTIONS
pragma to the top of your file:
{-# OPTIONS -Wno-warn-unused-binds #-}
module Foo where
...
Or on GHC versions prior to 8.0, as well as current GHC for backwards compatibility:
{-# OPTIONS -fno-warn-unused-binds #-}
module Foo where
…
On -Werror
The -Werror
flag causes GHC to treat warnings as errors. If you're developing an application, I recommend enabling -Werror
only for CI and production builds. This gives you the safety you want in production without disrupting your development workflow, by, say, requiring every unused import and variable to be used.
I’m less certain about developing a library, but I believe the best approach is to enable -Werror
only in CI. This is because users of your library (either humans or CI systems like Stackage) will eventually be trying your library on new versions of GHC. Either because you're using -Weverything
or -Wcompat
, or just because you've enabled -Wunused-imports
, new GHC versions are very likely to trigger warnings that can be safely ignored. When those occur, it's frustrating to the end-users to run into these spurious build failures.
Bonus Warnings!
While writing this blog post, I learned of some additional warning flags for GHC’s internals: -dcore-lint
, -dstg-lint
, and -dcmm-lint
. It's not listed next to the other lint flags, but -dasm-lint
also appears to be a thing.
Initially on reading the GHC user guide, which describes -dcore-lint
as being for the "really paranoid", I figured these almost never triggered, that they were warnings only for the Alastor Moody's of the world. So I ran them on our codebase, and got three findings before compilation stopped (output).
Assuming that these lints aren’t finding false positives, would it be helpful for more people to run them? I had never heard of them before, but something like Stackage could build all packages with those flags to look for GHC bugs.
† These numbers are from the counting the warnings at the top of the GHC user’s guide. But I think there’s a good chance those lists aren’t 100% accurate.
‡ That several warnings are missing from the GHC user guide’s list makes me suspicious that other warnings might not be, as well, but the only way to know for sure is to trigger them with -Weverything
and not with -Wall
(and if you do find any, please do submit a PR to the GHC Github Mirror, on the warnings page—it's very easy and can be done from Github instead of Phabricator).
Max Tagher is the co-founder and CTO of Mercury.