osa1 github gitlab twitter cv rss

Enable these two flags in GHC 8.2

July 15, 2017 - Tagged as: en, haskell.

As usual, the next major GHC release will be pretty great. It’ll come with a bunch of new features that I can’t wait to start using, and I’ve contributed to three of the new features1, but what excites me the most is not any of these features. I’m most excited about the improved -Werror flag.

The summary is that with GHC 8.2 we’ll be able to promote some warnings into errors, without making all warnings errors (which is how -Werror worked pre-8.2). With this we can finally fix some of the Haskell 2010 warts.


By the beginning of this year I moved from academia to industry. I was writing Haskell in academia, and I’m still writing Haskell, but the environment, tasks, and constraints are quite different, so the way I write Haskell changed quite a lot during this transition.

What I realized that some of the problems with Haskell 2010 are actually worse than I had previously thought.

Problem 1: Initializing records without initializing all of its fields

Haskell 2010 says that when initializing records using labels, fields not mentioned are initialized as bottoms.

I just can’t fathom how Haskell 2010 people thought this is a good idea. In Haskell we constantly rely on compile-time errors to refactor our code. A common workflow is this: you update your data types, and follow the type errors to adapt your code to the changes. Quite often your program works as expected after this. I did this countless times during my career as a Haskell programmer, and I’m trying to improve GHC to make this workflow even more efficient.

The problem is this “feature” breaks this workflow, because adding a new field to a type no longer generates a compile error. It generates a warning, but that’s not good enough because (1) not all projects have -Wall enabled (2) not all projects are warning-free, which means new warnings sometimes go unnoticed (this happens in our code base all the time).

Indeed, even very experienced Haskellers release buggy code because of this. For example, warp-3.2.10 added a new field (connFree) to one of its types (Connection), and for some reason only the minor version was bumped (3.2.9 to 3.2.10, which is probably wrong according to PVP because the type was exported). The problem was warp-tls-3.2.2 had 3.3 as warp upper bound, so it compiled fine against warp-3.2.10, even though it didn’t initialize the field. This caused bugs in our system, which we thankfully discovered in our test environment rather than on production. The fix was easy, but the damage was done (the buggy warp-tls-3.2.2 is still on Hackage).

Problem 2: Non-exhaustive pattern matching

While I can’t find any mention to exhaustiveness of pattern matching in Haskell 2010, it clearly covers the case where patterns do not cover all values when defining formal semantics of pattern matching (see case (b)). You only realize that this is a bad idea when (1) your code is not warning-free so new warnings sometimes go unnoticed and (2) you can’t promote individual warnings to errors. This again breaks the workflow I mentioned above, and makes code reviews much harder.


The solution that GHC 8.2 brings is we can now make these two warnings errors, using -Werror=missing-fields -Werror=incomplete-patterns. There’s still one problem though, the error message is not good enough. Suppose we had this code:

module Lib where

data Rec = Rec { f1 :: Int, f2 :: Int }

data S = C1 Int | C2 Int

-- incomplete pattern
sInt s = case s of
           C1 i -> i

-- missing field
initRec = Rec { f1 = 1 }

Compile this with ghc -Wall and you get:

[1 of 1] Compiling Lib              ( test.hs, test.o )

test.hs:11:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature: sInt :: S -> Int
   |
11 | sInt s = case s of
   | ^^^^

test.hs:11:10: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In a case alternative: Patterns not matched: (C2 _)
   |
11 | sInt s = case s of
   |          ^^^^^^^^^...

test.hs:15:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature: initRec :: Rec
   |
15 | initRec = Rec { f1 = 1 }
   | ^^^^^^^

test.hs:15:11: warning: [-Wmissing-fields]
    • Fields of ‘Rec’ not initialised: f2
    • In the expression: Rec {f1 = 1}
      In an equation for ‘initRec’: initRec = Rec {f1 = 1}
   |
15 | initRec = Rec { f1 = 1 }
   |           ^^^^^^^^^^^^^^

We only care about missing fields and incomplete patterns, so with GHC 8.2 we compile this with ghc -Wall -Werror=missing-fields -Werror=incomplete-patterns, which generates the same warnings, but the process exits with non-zero, and prints these extra lines:

<no location info>: error:
Failing due to -Werror.

This is not too useful, because if you get dozens of warnings there’s basically no way of knowing which of those warnings caused this error. One alternative is to disable -Wall and only use -Werrors. That way you know that the warnings you’re seeing are actually errors.

Still, this is not entirely satisfactory, because even though we don’t cause our build to fail when we have warnings, they’re still sometimes useful to see (for example, name shadowing warnings often catches accidental loops). So to improve this I recently submitted a patch, which is merged, but unfortunately won’t make it to GHC 8.2 (hopefully we’ll see it in GHC 8.4). With that patch when you have both -Wall and some -Werrors, you see this instead:

[1 of 1] Compiling Lib              ( test.hs, test.o )

test.hs:11:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature: sInt :: S -> Int
   |
11 | sInt s = case s of
   | ^^^^

test.hs:11:10: error: [-Wincomplete-patterns, -Werror=incomplete-patterns]
    Pattern match(es) are non-exhaustive
    In a case alternative: Patterns not matched: (C2 _)
   |
11 | sInt s = case s of
   |          ^^^^^^^^^...

test.hs:15:1: warning: [-Wmissing-signatures]
    Top-level binding with no type signature: initRec :: Rec
   |
15 | initRec = Rec { f1 = 1 }
   | ^^^^^^^

test.hs:15:11: error: [-Wmissing-fields, -Werror=missing-fields]
    • Fields of ‘Rec’ not initialised: f2
    • In the expression: Rec {f1 = 1}
      In an equation for ‘initRec’: initRec = Rec {f1 = 1}
   |
15 | initRec = Rec { f1 = 1 }
   |           ^^^^^^^^^^^^^^

Much better!

This is probably not as exciting to many people as, say, new features like compact regions or join points, but I think this will significantly improve “refactor types, folow type error, repeat” style workflows and make code reviews much easier.


  1. I discovered and reported #10598 two years ago, which led to -XDerivingStrategies work, I was involved in the Compact regions paper, and I implemented unboxed sums during my time at MSR Cambridge last summer.↩︎