Improved Type Error Messages #156
Conversation
I agree with @goldfirere this is probably the best venue to get feedback from those who more read than write compiler messages. (I have similar experience from mailing lists, which is sad commentary, but for another venue ...)
I'm neutral on the idea of using tags; strongly against putting them in square brackets, because they look like list types -- especially with the word
Should GHC express a personality in its messages? The Avoiding technical terms is certainly valuable. (But that's down to individual messages rather than a general overhaul?) As @gbaz points out, "untouchable" and "rigid" are particularly ornery. I just know them as that error I don't understand but do know how to fix. (There's heaps of questions on StackOverflow about "untouchable"/"rigid"; perhaps we can steal some words from answers there?) |
I dislike the I like proper English words as @AntC2 suggests:
I even think the "Couldn't match expected type with actual type" part of the message is redundant. Words "expected" and "actual" are clear enough. So we can simply have
I like shorter error messages: sometimes GHC reports too much and I have to scroll. |
As to the tooling motivation, I've had enough with tools trying to parse messages intended for humans. Parsing is the worst, and error messages don't have a formal grammar (and shouldn't have!) Whoever wants good tooling should stop messing around and start adding JSON error message output format. |
I agree that caret positions subsume the In... stuff, and welcome shorter, more concise error messages when it does not lose important information |
FYI, Elm has a similar syntax and has put a lot of time into making nice error messages. We could get inspiration from there. Here's what it outputs for
|
What I like about this particular error message: It makes no mention of expected / actual, which requires me to think from the perspective of the compiler. I still have some trouble with this, and I much prefer the above, which just tells me what the type annotation is (expected) and what the value's type is (actual). It's wordy, but meaningful to my human brain |
Here's an example of type inference kicking in, and again no mention of expected / actual:
|
@NadineTheBean, does your work cover type holes as well? I'd love to see some improvements here :)
I feel this can be simplified to something like:
|
Count me among those who have been confused by "expected" and "actual." For example, when I'm stuck writing code that needs a type signature for disambiguation and I get that signature wrong, the fact that my wrong signature is the "expected" type throws me off every time. Elm's errors are perhaps more verbose than I'd prefer, but they're very clear. |
@tejon Interesting. Why? For me it's clear: when I write a type signarute, I tell the compiler to expect a term to have that type. Regardless of expectations, there's the actual type which it infers. Both of these types could be correct or incorrect. I find it all very intuitive and never had a problem even as a beginner. Could you please share your thinking process? Why are these words confusing to you? Can you think of better ones? Here's one idea: perhaps it would be more clear with words like "specified" and "inferred"? They sound a bit more technical, though. |
I too ignore what GHC is labeling as expected and as actual, and try to make sense of it otherwise... so I would probably benefit from a different wording. |
I recently wrote some code where it was easy to construct infinite types and I happened to me a few times. I remember that I was very frustrated with GHCs error because I couldn't figure out which part of the code was responsible for the infinite type from the error message. I can't give you example unfortunately. |
I believe that this is one of the issues with reviewing type errors. I am currently working on type errors for my PhD and trying to gather "real-life" examples is difficult as most do not keep to share or even push to github when a type error happens, when you do get examples you do not get the "feel" behind why it was difficult to solve, just that it was not understandable. I feel that having such a collection that contained these insights would be beneficial in seeing which errors are most problematic, however, understand that they would also be hard (near impossible) to collect! |
That's exactly it: as @mitchellwrosen said, this error is from the compiler's perspective. To truly understand it, I have to shift to that perspective. The thing is, in the vast majority of cases, I don't really have to understand it. My brain can take a shortcut, applying these same terms at a code level: the "expected" type is one that a function expected, and the "actual" type is the one that I provided to a function, and this carries a whole set of implications about where to look for the root cause of error. And that heuristic does not include "bad type signature" because it's just too uncommon to make it into my first-pass debugging habits. Yes, I can shift into "think like the compiler" mode and figure it out. But my understanding at that level is abstract. I've never written a compiler; I learned programming from the other end, starting as a hobbyist. The transition has non-trivial cost, I work slower at that level, and most of the time I don't need to do so to resolve this error. In fact, most of the time I can just parse it structurally and not really read the keywords at all, which is vastly more efficient. The confusion and frustration happens in the period between that heuristic failing, and me remembering that it's just a heuristic and shifting gears. Changing terminology to something that doesn't make as much sense outside the compiler mindset probably would help (and this isn't the first time I've thought about it). The question is whether there's any terminology that is unambiguously compiler-centric and always correct. Replacing "actual" with "inferred" is the best thing that's come to mind so far, but I'm not sure it's the right term in every case? Consider: foo :: Int
foo = 7
bar :: Bool -> Bool
bar x = foo && x In Other remappings I've thought of:
I doubt "required/requested" or "discovered" would help, since they also map easily to function-level thought. "Specified" is on the line there, and I'm not sure its meaning is clear (or correct) enough in any case. All told, if nobody with more expertise sees an issue with "inferred" instead of "actual," I think I'd recommend that change at minimum. But I should emphasize that this is not a replacement for thinking more about Elm's errors as a directional motivation. Edit: @int-index In the course of typing this I completely forgot about your last paragraph, where you also came up with "specified" and "inferred." As I mentioned, this is not the first time I've thought about exactly this topic, and those terms were already in my head prior; point being, we landed on them independently. That does seem like a point in favor! |
I agree with leaving out the redundant "In the .. " context messages. However, the proposal omits the useful
In this case the compiler is telling us which part of the unification failed. If the expected and actual types are large and complex, then figuring out which part didn't match can sometimes be really hard. Now, perhaps the current formulation also isn't ideal, it would be a lot easier if the compiler underlined the offending portions of the types, or rendered them in a different colour or something. But I don't think we should discard this information. e.g.
In fact there might be a complicated edit between the two types, so we might have multiple parts underlined. Colours would make this a lot easier to comprehend. |
Thank you for the feedback so far everyone, I really appreciate it! :)
|
I think the terms So perhaps some sort of neutral terminology would be better, using |
What about
|
I like Bonus option: pair |
I think I like
If the types are long, use
Not sure if the prettyprinter supports alternate forms when there's a horizontal overflow, but this is what you'd get in that case. |
I can definitely get on board with that. |
@mgsloan Even if
|
My 2 cents into this proposal. If we have code like this: example2 :: (Bool,Char,Bool,Char,Bool,Char) -> (Bool,Char,Bool,Bool,Char,Char)
example2 (a,b,c,d,e,f) = (a,b,c,d,e,f) then it would be cool to have error message like this (inspired by multiple sources): • Could not unify expected type [E]
[E] (Bool, Char, Bool, Char, Bool, Char)
^^^^ ^^^^
and actual type [A]
[A] (Bool, Char, Bool, Bool, Char, Char)
^^^^ ^^^^
in the code below:
C:\Users\Example\Documents\Examples.hs:44:10-15
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43 ┃ example2 :: (Bool,Char,Bool,Char,Bool,Char) -> (Bool,Char,Bool,Bool,Char,Char)
┃ ^^^^ ^^^^
44 ┃ example2 (a,b,c,d,e,f) = (a,b,c,d,e,f)
┃ ┃ ┗━ e :: [E] Char
┃ ┃ [A] Bool
┃ ┃
┃ ┗━ d :: [E] Bool
┃ [A] Char |
6b33e58
to
e7fdbc7
Rendered