Debugging Haskell Type Errors

https://jelv.is/blog/Debugging-Haskell-Type-Errors/

We can debug Haskell type errors systematically: fixing type errors is a skill you can learn.

Here are the three principles I use to fix tricky type errors.

I had a thread earlier asking for examples of type errors. Folks gave me a bunch of examples (thank you!) but they mostly did not fit the post I wanted to write. Originally I included a bunch of these examples in this post, but it became way too long; I’m planning to write a follow-up article just covering the patterns people suggested.

14 Likes

There already was a discussion about removing the redundant “In the first argument of …” snippets here: Type error message overhaul · Issue #541 · haskell/error-messages · GitHub I think there were also other places where the removal of these lines was discussed, and I don’t remember anyone giving any good reasons why they should stay, except for the case of error messages for TH generated code. Personally I have also learned to completely ignore them.

I think this great blog post is as good an occasion as it gets to start the discussion again on whether we should finally pull the trigger and remove these lines, or whether there still is a genuine usecase. Maybe what is missing is some forum for the discussion which can finally bring this issue to a final decision, maybe a GHC proposal?

3 Likes

Oh yeah, I’m 100% in favor of getting rid of them. Even if I’m in a context where they’d be helpful in principle, they’re so noisy and hard to read that they’re not worth it. It’s easier to open my editor and/or GitHub and manually navigate to the corresponding file and line number. (I probably need to see more of the context anyways!)

In the case of Template Haskell specifically, we should probably just dump a bunch of the code directly instead of just giving out breadcrumbs. But I guess that might be a hard change in the code?

Talking about hard changes in the code the idea thing would be to have a structured error representation so that we could have different messages in different contexts (ghc vs ghci vs langauge-server vs etc), but I imagine that would require a massive refactoring :confused:

5 Likes

You can, in fact, remove this context by using the -fno-show-error-context flag. I’m guessing they just did not want to change the default…

So as an example, say I have f :: Char -> Bool and I try to use f "bla", then I will get

<interactive>:2:3: error: [GHC-39999]
    • No instance for ‘GHC.Internal.Data.String.IsString Char’
        arising from the literal ‘"bla"’
    • In the first argument of ‘f’, namely ‘"bla"’
      In the expression: f "bla"
      In an equation for ‘it’: it = f "bla"

and with -fno-show-error-context I will get

<interactive>:4:3: error: [GHC-39999]
    No instance for ‘GHC.Internal.Data.String.IsString Char’
      arising from the literal ‘"bla"’
7 Likes

This already exists and HLS is in the process of switching to this structured error format, as far as I understand :stuck_out_tongue:

2 Likes

Thanks @Tikhon. I found your terminology … em not altogether the terminology I would use:

  • I think of a ‘bug’ as being where the program compiles (possibly with some warnings) and runs; but
  • doesn’t produce the out put expected, or what the spec says. (Or what the spec would say if the client had thought of this scenario in advance …)
  • ‘Bug’ includes things like running forever or stack overflow or numeric overflow.
  • That Nine Rules book you mention says “Debugging usually means figuring out why a design doesn’t work as planned.” – which at least means it “works” somewhat. A program that doesn’t compile is nowhere near any kind of “working”.

What you’re describing I’d call ‘type mis-matches’/‘compile fails’ – error messages from the compiler because type unification fails. (Your write-up explicitly mentions Hindley-Milner type systems, but not unification – see the link from here on H-M.)

Haskell Types as Constraints’ – beware. ‘Constraint’ has a very specific meaning within Haskell’s type (class) system. Most of your examples in that section do not involve typeclasses.

Your ‘Type Signatures as Assertions’ example for me rather adds to my confusion. (Also beware ‘Assertion’ is another technical term in some programming languages, usually about values not types):

An important aspect of Haskell’s type checking and type inference is that type signatures act like assertions. That is, when Haskell sees x :: Int, it will take this as given for the rest of the code. This is true even if x is defined to be something that can’t be an Int.

If we load the following code, we’ll get two type errors: …

Ok you’ve set me up to expect to see x :: Int. So at first reading I’m perplexed why you didn’t and why those two messages don’t talk about the x = False line. You do go on to do that afterwards, but by then I’m bamboozled by the No instance for (Num Bool) arising from a use of ‘+’ message. (Or at least I would be if I were a beginner who doesn’t already know what you’re doing there.) I suggest you cover first what happens with the signature; then go on to the more bamboozling messages due to omitting the signature. That’s reinforcing the best practice of giving signatures. (And that’s one diagnostic technique: give signatures for every variable; also put inline signatures :: for every call to a class method.)

BTW 1:

(Side note: that second error is a great example of arbitrary attribution: why does it point to + and not * as the reason we need a Num instance? …

Yeah, good case in point of error reports hand-waving to the general vicinity of the mis-match rather than anywhere precise. Try this for your last line if you want a totally misleading message:

z = 2 > x + y

BTW 2: Is deferring type errors a Good Thing to be suggesting to your audience?

  1. This type error is actually a warning because I have the -Wdeferred-type-errors flag turned on. This flag is great for development because it lets the compiler surface more type errors and lets you experiment with the working parts of your code even if other parts don’t typecheck.:leftwards_arrow_with_hook:

Doesn’t deferring tend to increase the number of errors reported? (In a knock-on effect from one error.) And make it more difficult to narrow down where the initial cause is?

3 Likes

Thanks for mentioning this! I had no idea this flag existed. Apparently it was added in GHC 9.8 due to this MR:

3 Likes

Oh sweet, I didn’t know that. I’ll update the footnote with that information.

I’ll have to figure out how to make that the default for all my tools!

1 Like

Yeah, I suppose the terminology might be a bit confusing, but that seems hard to avoid—the CS world just has its share of overloaded vocabulary.

Is deferring type errors a Good Thing to be suggesting to your audience?

That flag has been my suggestion for everyone I’ve taught and it seems to work well in practice even for beginners. (Not to say I have the largest sample size here!) Seeing more errors might be a bit confusing at the very beginning, but seems like it’s actively useful as soon as you get in the habit of at least scanning every error message before diving in to fix anything.

Sweet! I imagine that required a bunch of effort, so big kudos to the folks that did the refactoring.

You say:

an error message can only point to one, so the compiler has to choose somehow.

but I think that paradigm could easily be challenged :slight_smile:

2 Likes

We actually don’t use -fno-error-context in HLS yet, although it would definitely help. There is a stallled PR here (it turned out to be harder than it sounds!), and we’d definitely welcome someone picking that up again! Use -fno-show-error-context from GHC 9.8 by dsaenztagarro · Pull Request #4295 · haskell/haskell-language-server · GitHub

4 Likes