Engineering

How to enable all the warnings in Haskell

Written By

Max Tagher

Illustration of code snipped for Mercury Engineering blog
Copy Link
Share on Twitter
Share on LinkedIn
Share on Facebook
Linegraph tracking a Mercury account balance
Build the next generation of startup bankingExplore Openings*Mercury is a financial technology company, not a bank. Banking services provided by Choice Financial Group and Evolve Bank & Trust®; Members FDIC.
Copy Link
Share on Twitter
Share on LinkedIn
Share on Facebook

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:

Copy Code
-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:

Copy Code
-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.:

Copy Code
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:

Copy Code
(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:

Copy Code
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. Int32to 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:

Copy Code
f :: (Eq a, Ord a) => a -> Bool -- Ord implies Eq, so Eq is unnecessary

and when a constraint is unused by the function body:

Copy Code
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.

Copy Code
#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:

Copy Code
:set -XMonomorphismRestriction
set -Wmonomorphism-restriction
f xs = (len, len) where len = Data.List.genericLength xs

will trigger this warning:

Copy Code
<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:

Copy Code
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:

Copy Code
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:

Copy Code
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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.

Copy Code
// 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:

Copy Code
{-# OPTIONS -Wno-warn-unused-binds #-}
module Foo where
...

Or on GHC versions prior to 8.0, as well as current GHC for backwards compatibility:

Copy Code
{-# 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 -Werroronly 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 -Weverythingand 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).

Notes
Written by

Max Tagher is the co-founder and CTO of Mercury.

Share
Copy Link
Share on Twitter
Share on LinkedIn
Share on Facebook