Reading GHC Errors
One of the things beginners find difficult about Haskell is reading compiler errors. This is fully understandable for many reasons.
- ghc prints notoriously detailed error messages.
- Haskell lets us write very generic code so type errors can get complicated.
- Furthermore, when the types don’t match, the compiler cannot know which type is correct and which is wrong.
- 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.
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.
- 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.
- 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.
- 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.
- 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. - 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 callselectList
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
Following the same steps as before:
- 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.
- 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_
, namelyalternatives
”. ThemapM_
function is a type of loop, so it seems reasonable that this is related to the$forall
template directive, and thenalternatives
is the list we wanted to loop through. - We compare “expected” to “actual” types and see that somewhere it’s seeing a
list of
Entity Proposal
but it wanted to get aProposal
. - 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 ofProposal
but the compiler cannot match[Proposal]
against the type ofalternatives
. - 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.
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.
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.
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:
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:
“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.
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.
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.
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.
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
- It is fine to get errors. Get many errors! It helps you shape your code into something that does the right thing.
- The yellow highlight marks the locations that contain most information in an error.
- When in doubt, insert an underscore to get further suggestions from the compiler.
- 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.