Entropic Thoughts

Reading GHC Errors

Reading GHC Errors

One of the things beginners find difficult about Haskell is reading compiler errors. This is fully understandable for many reasons.

  1. ghc prints notoriously detailed error messages.
  2. Haskell lets us write very generic code so type errors can get complicated.
  3. Furthermore, when the types don’t match, the compiler cannot know which type is correct and which is wrong.
  4. Popular Haskell libraries recommend using macros to generate code, which complicates error messages further.

Whenever there’s a compilation error, ghc tends to spit out a wall of text. Experienced Haskell users know quickly which pieces of this wall of text are informative, and which can be ignored. This is my attempt at helping beginners learn that skill.

This article is a collection of a few error messages I got in one session of writing a hobby project, and how I chose to read them.

You’re meant to read a lot of errors

One thing should be clear: getting a compiler error when using Haskell is not considered a bad thing. When using other programming languages, compiler errors are slaps on the wrist: we did something wrong and the compiler tells us off for it. With Haskell, it’s a good idea to look at compiler errors as a discussion we’re having with the compiler. We’re proposing some code, and the compiler says, “What you have written implies X. Is this really what you intended to do?” and we go back and forth with the compiler until both us and the compiler agree that the code we wrote and our intention match up.

It’s not strange in Haskell land to write some code we are unsure of and hit compile to see what the compiler has to say about it. Then we iteratively refine-and-compile in a tight cycle. This is why Haskell users say “when the code compiles, it works.” It’s not that it works because it has compiled, but rather that once it compiles, we’ve had a back-and-forth discussion with the compiler for several iterations so we have a fairly clear idea in our head what it is we’re trying to do, and that increases the chances of us having done it right.

Let’s jump into it.

Forgot to supply a function argument

We just wrote two lines of code, recompiled, and got this thrown at us.

ghc-error-01.png

The yellow higlight indicates which parts of this we should look at. This is a general five step process that we can follow with most compiler errors.

  1. Confirm the file name and line number of the error location, given at the top of the error message. Actually navigate to this location in the editor, to confirm that we and the compiler are indeed talking about the same code.1 It has happened that I’m convinced the error is in some location that looks similar to the actual location of the error, and I stare myself blind at the wrong code. Starting by navigating to the location given by the compiler avoids this.
  2. Read the first level of description of the location where the error occurred, in the middle of the error message (“In the second argument of”). This helps us narrow down to the specific expression that errors.
  3. Compare the “expected” type with the “actual” type as given in the compiler error. Remember that the compiler does not actually know which type is correct. The “expected” type is not necessarily correct – it could just as well be the result of mistaken inference. In other words, don’t read too much into the names “expected” and “actual”, but do compare them to understand in which way the two types mismatch. When comparing expected to actual types, do not read the full type. Instead, compare the overall shapes of the types. In this case, the “expected” type is a single value, but the “actual” type is a function from a list to some value.
  4. Figure out whether the “actual” or “expected” types are correct. In this case, the runDB function which we are trying to call has a type signature that asks for the “expected” type, so we know the “expected” type is the one we want.
  5. Discover how to get from the wrong type to the correct type. The problem in this case seems to be that we are trying to give a function as an argument when the parameter is supposed to be a plain value. If we look at the documentation of selectList we’ll realise we forgot to give it its second argument. Doing so will call selectList and give us its return value back. This makes the code compile.

It might seem surprising that this solves the problem, because it doesn’t seem like the values would line up – the expectation is a YesodDB App something but the actual value we’ll get back from selectList seems to be a ReaderT backend something. These two types are – apparently – synonyms. There are some hints of this in the compiler error, but the way we know for sure is that the code compiles once we supply the second argument to selectList.

Note that we don’t look at the squigglied code at the bottom, because the squigglies are frequently misleading. In this error, the squigglies seem to indicate that there’s something wrong with the function call we have – but there’s not! What’s wrong is the missing second parameter, which would go to the right of the squiggled code.

In this case the compiler also suggested a “probable cause” which happened to be correct, but we shouldn’t generally put too much emphasis on this because (a) it doesn’t tell us anything the type error didn’t, and (b) it can be misleading in cases where the “actual” type is correct and the “expected” type is wrong.

Trying to pattern match against the wrong type

Next up, we have written a line of Hamlet template code.

$forall Proposal name _ <- alternatives
  <li>#{name}

When building, the compiler tells us that

ghc-error-02.png

Following the same steps as before:

  1. We confirm the location in our editor. In this case the reported location is not very accurate: it only points to the start of the template. Since the tempalte is written inside a macro call, the actual error comes from code generated by the macro.
  2. We read the first level of the error location description. It is really helpful in this case since we didn’t get an accurate line number. It refers to “the second argument of mapM_, namely alternatives”. The mapM_ function is a type of loop, so it seems reasonable that this is related to the $forall template directive, and then alternatives is the list we wanted to loop through.
  3. We compare “expected” to “actual” types and see that somewhere it’s seeing a list of Entity Proposal but it wanted to get a Proposal.
  4. We figure out whether “expected” or “actual” are correct. In this case, “actual” must be correct because we had assumed the alternatives list would be a list of Proposal but the compiler cannot match [Proposal] against the type of alternatives.
  5. Since we know now we have a list of Entity Proposal we’ll update the pattern match to conform to this.
$forall (Entity _ (Proposal name _)) <- alternatives
  <li>#{name}

How do we know this change accomplishes that? Unfortunately that must sometimes come from knowledge of the library we use.

Type conversion required

We have a Decision record that was fetched from the database, and we’d like to do additional lookups based on its database key. There’s a function decisionKey that extracts this key from the record, but when we try to assign it to a variable holding a database key for decisions, we run into a compiler error.

ghc-error-03.png

The raw key we get out of the Decision record appears to be a regular Haskell value of type Word64, rather than the type of managed database key expected by the database library. This is another case in which the “actual” type is correct, and the “expected” type is wrong, and we know because we specifically requested a variable of type Key Decision.

Since we are new to this database library, we might try to search the web for how to make a database key from a Haskell value. If we’re lucky, we find the one StackOverflow question where someone has a similar problem. The answer indicates that there should be a constructor called DecisionKey that can do this for us. However, when we compile with that in there, we get another error.

ghc-error-04.png

This is similar to the first error in this article except in reverse: it expected a function from Word64 to Key Decision (yay it understood our intention) but it seems like DecisionKey was not this function. Since the StackOverflow question we found was the only good result for our search, we’ll have to try to brute force our way from here.

We replace DecisionKey with a single underscore. This creates a typed hole, which asks the type checker for which things are in scope that might fit with the requested type.

ghc-error-05.png

Look at that! The compiler suggests we use DecisionKey' with a prime instead. Maybe this constructor has been renamed since the StackOverflow question was asked.

This is a really powerful feature. Whenever you’re unsure what to write anywhere in a Haskell program, try just writing a single underscore, and see if the compiler suggests the right thing for you!

Wrong function used

In getting the next error, we were very sloppy. We have a list of tuples. We want to sum up the number in the third element of the tuple, and compute how far it is from three. We tried writing

let myRemaining = 3 - sum (\(_, _, i) -> i) alternatives

but we get this very big error:

ghc-error-06.png

We see something about ambiguous type variable around the constant 3, and this is a common problem. The constant 3 is not an integer in Haskell, it’s any numeric value that can be constructed from an integer. This could be an Integer, but it could also be a Float, a Word8, a CLong, a Complex Double, etc. So we sometimes have to be explicit and say e.g. (3 :: Int) to get past this type of error.

If we do that in this case, however, we discover that the ambiguous type error was a red herring! The real problem is below:

ghc-error-07.png

“No instance for Foldable … arising from a use of sum” is the technical way of saying “the argument to sum was not a collection type”. And sure enough, we accidentally passed a function to sum, thinking it acted like a sumBy function. There is no sumBy function, but we can get what we want by combining foldMap with the Sum monoid. If we’re sloppy when we try, we run into another error.

ghc-error-08.png

This uses complicated words to tell us that we’re trying to use the type name Sum as a function. The reason this mistake can happen is that there is a type name called Sum, and its constructor function is also called Sum. Normally, the compiler knows which one we mean, but it’s easy to accidentally import only the type name, and forget to import the constructor. This happens if we write

import Data.Monoid (Sum)

instead of

import Data.Monoid (Sum(..))

where the latter imports the type and all its constructors. Once we do that, however, we get the next error.

ghc-error-09.png

This is another variant of that “No instance for F X arising from Y”. This is the compiler’s way of telling us that the function Y required X to implement the interface F, but it does not. This means one of two things:

  • Either X is supposed to implement F but does not – then we can implement F for X.
  • Or, perhaps more commonly, we wanted to pass something that implements F, and X is not it. Then we need to transform X into something else.

In this case, the latter is the correct approach. The compiler is saying that it doesn’t know how to convert a Sum to html. That’s fine, because we can extract the numeric value from the Sum with the getSum function, and the compiler knows how to convert numeric values to html. Once we do that, the code compiles.

Unnecessary function call

Now we have written a loop and we’re not at all sure what we’re doing. We suspect it constructs a nested list of some sort, and we think it might return a nested side effect … or something. We try to map concat over it to unnest the list. We have no idea what we are doing, and the compiler catches us.

ghc-error-10.png

We won’t even bother most of the error message because we’re so out of our depth.

To be clear: this is a valid way to use Haskell. We’re free to throw things at the compiler and see what sticks. This back-and-forth of error messages should be thought of less as a slap on the fingers and more as a conversation with the compiler asking “What did you intend here?” or “Do you mean like this?”

Let’s say we’re not sure what to make of this error because we had no idea what we were doing. We can then insert a type wildcard on the bound variable, to see what the compiler thinks we have.

ghc-error-11.png

It looks like the result this loop binds to the variable is a plain nested list. This means the expression itself produces a single, top-level side effect. We had the right idea, but the <$> fmap operator was mistaken because it interfered with the $ application operator used later in the expression. We can change concat <$> to something like fmap concat . to solve the precedence issue and things will be fine again.

Takeaways

These were some examples of more and less hairy ghc errors that I encountered in a single working session. The things to take away are

  1. It is fine to get errors. Get many errors! It helps you shape your code into something that does the right thing.
  2. The yellow highlight marks the locations that contain most information in an error.
  3. When in doubt, insert an underscore to get further suggestions from the compiler.
  4. Follow the five-step process to resolve the problem.

Of course, somewhere in that five step process is the step where we draw the rest of the owl. With experience, that goes faster too.