Inspired by this reddit discussion, I'm wondering if there are any technical obstacles in the way of a "Dr Scheme"-style approach to assisting beginners when it comes to comprehending error messages. A case in point is the notorious
Prelude> 1 "doesn't"
<interactive>:3:1:
No instance for (Num ([Char] -> t0))
arising from the literal `1'
Possible fix: add an instance declaration for (Num ([Char] -> t0))
In the expression: 1
In the expression: 1 "doesn't"
In an equation for `it': it = 1 "doesn't"
Suppose we were to make the local assumption that the programmer will declare no new instances. Indeed, one can get a long way in Haskell interacting with only prelude classes only via deriving, so this assumption is not unrealistic for beginners. Under such an assumption, the above error message would be beside the point. Could we improve upon it? We might still need to figure out how much to say about the type of 1, but we could surely be more direct about the problem.
Are there other opportunities to reframe error messages on the basis of realistic simplifying assumptions? Note, I'm asking a question about changing the text of error messages based on a model of the programmer's experience: I am not locally considering any changes to which code is considered erroneous (e.g., by assuming that beginners will use overloaded things at specialised types, eliminating ambiguity).
A follow-up thought: does the text of the existing error messages contain enough information to support such transformations, assuming a "configuration file" modelling the student? That is, could an enterprising hacker implement an intelligibility-enhancing postprocessor for ghci without troubling the busy people at HQ?
I'm not convinced that ignoring certain features for the sake of error messages would be helpful.
For one thing, are the errors encountered by beginners really that different from anyone else? Just because I know how to interpret a "no Num instance for this absurd thing which is clearly not a numeric type" error doesn't mean I wouldn't appreciate GHC more clearly pointing out what bone-headed thing I've done to provoke such a message.
I would suggest that the circumstances in which a beginner or an expert would be writing instances, or any other kind of "advanced" code, differ very little. The beginner, however, will be coding under those circumstances far less often, with an inclusive lower bound of "never".
If one wishes to insulate beginners from arcane corners of the language, then make sure they don't encounter them at all. For instance (ha, ha), there's an argument to be made for avoiding type classes entirely when first introducing the language.
I can't think of any cases where a more beginner-friendly error message, so long as it doesn't throw away information, would not also be more pleasant for everyone else as well.
Suppose we were to make the local assumption that the programmer will declare no new instances.
Suppose we instead make the assumption that the programmer will declare no orphan instances where the class and/or type are defined by the Haskell Report. This assumption is most assuredly realistic for beginners and, in fact, seems reasonable as a general rule.
Adding such an instance because of a compiler error at all is plausible in very few situations. With the glaring and galling exception of the anonymous writer monad (unless that has been remedied since I last checked) orphan instances for a standard class and type seems exceedingly unlikely, and one or the other should only arise when a library has unexpectedly overlooked a few instances, or is old and dusty enough to predate the prevalence of stuff like Applicative or Foldable. In any case, anyone who does want such an instance can be safely assumed to know what they're doing well enough to interpret GHC's error message, whatever it may be.
But enough about that particular example.
Are there other opportunities to reframe error messages on the basis of realistic simplifying assumptions?
Very likely, but we're also very far from exhausting the space of realistic complicating assumptions!
The point of the above digression on instances is that by using information already available to the compiler and heuristics that examine a larger context (e.g., where certain things are defined, to distinguish the above example from non-orphan instances) error messages could be improved in general.
A follow-up thought: does the text of the existing error messages contain enough information to support such transformations, assuming a "configuration file" modelling the student?
I really can't see this working very well except in the simplest and most egregious cases, the example you gave being a notable example. It could rephrase existing flavors of error, but beginners would benefit most from having more detailed/explicit information that the existing message may not contain.
Consider two other error messages that are chronically vexing to beginners: complaints about matching "rigid type variables", and complaints about "infinite types". Rephrasing might improve those somewhat, but short of including a tutorial on polymorphism in each error message it's not going to much help a beginner fix their code, unless you can interpret the relevant expression and type well enough to be specific about the cause.
Together with inspecting the source code a "translator" might have some legs, but I suspect modifying GHC directly will quickly become the easiest route. Generating error messages would be an interesting extensibility point, but I don't think this can be done with GHC's existing plugin infrastructure.
The greatest obstacle to any of this, I expect, is not implementation. Rather, it's the decision of which heuristics would improve error messages rather than accomplishing the opposite. Without a base of objective data, this is something of an error-prone (ha, ha) guessing game. It would be very interesting to assemble a large survey of errors encountered by (primarily intermediate or novice) Haskell programmers and, most importantly, what actual change they made to resolve the issue.
I believe that the Helium subset of Haskell is an attempt to do this. It has been a long time since I have tried it but IIRC, it produced much better error messages than GHC.
Related
I have been playing around with LiquidHaskell and Idris lately and i got a pretty specific question, that i could not find a definitive answer anywhere.
Idris is a dependently typed language which is great for the most part. However i read that some type terms during type checking can "leak" from compile-time to run-time, even tough Idris does its best to eliminate those terms (this is a special feature even..). However, this elimination is not perfect and sometimes it does happen. If, why and when this happens is not immediately clear from the code and sometimes has an effect on run-time performance.
I have seen people prefering Haskells' type system, because it cannot happen there. When type checking is done, it is done. Types are "thrown away" and not used during run-time.
What is the story for LiquidHaskell? It enhances the type system capabilities quite a bit over traditional Haskell. Does LiquidHaskell also inject run-time bits for certain type "constellations" or (as i suspect) just adds another layer of "better" types over Haskell that do not affect run-time in any shape or form.
Meaning, if one removes the special LiquidHaskell type annotations and compiles it with standard GHC, is the code produced always the same? In other words: Is the LiquidHaskell extension compile-time only?
If yes, this seems like the best of both worlds, or is LiquidHaskell just not as expressive in the type system as Idris and therefore manages without run-time terms?
To answer your question as asked: Liquid Haskell allows you to provide annotations that a separate tool from the compiler verifies. Code is still compiled exactly the same way.
However, I have quibbles with your question as asked. It can be argued that there are cases in which some residue of the type must survive at run time in Haskell - especially when polymorphic recursion is involved. Consider this function:
lots :: Show a => Int -> a -> String
lots 0 x = show x
lots n x = lots (n-1) (x,x)
There is no way to statically determine the exact type involved in the use of show. Something derived from the types must survive to runtime. In practice, this is easy to do by using type class dictionaries. The theoretically important detail is that there's still type-directed behavior being chosen at runtime.
We're urged to avoid partial functions with seemingly more emphasis in Haskell than other languages.
Is this because partial functions are a more frequent risk in Haskell than other languages (c.f. this question), or is it that avoiding them in other languages is impractical to the point of little consideration?
Is this because partial functions are a more frequent risk in Haskell than other languages (c.f. this question), or is it that avoiding them in other languages is impractical to the point of little consideration?
Certainly the latter. The most commonly used languages all have some notion of the null value as an inhabitant of every type, the practical effect being that every value is akin to haskell's Maybe a.
You can argue that in haskell we have the same issue: bottoms can hide anywhere, e.g.
uhoh :: String
uhoh = error "oops"
But this isn't really the case. In haskell all bottom are morally equivalent and we can reason about code as if they didn't exist. If we could catch exceptions in pure code, this would no longer be the case. Here's an interesting discussion.
And just a subjective addendum, I think intermediate haskell developers tend to be aware of whether a function is partial, and to complain loudly when they are surprised to find they were wrong. At the same time a fair portion of the Prelude contains partial functions, such as tail and / and these haven't changed in spite of much attention and many alternative preludes, which I think is evidence that the language and standard lib probably struck a pretty decent balance.
EDIT I agree that Alexey Romanov's answer is an important part of the picture as well.
One reason why partial functions are significantly worse in Haskell compared to other languages is the lack of stack traces by default. When you call e.g. head on an empty list, you only get Prelude.head: empty list. Good luck figuring out which call of head is the problem or where the empty list came from! Of course, it may not even be in your code, but in some library you are using.
To get a stack trace, you need to either compile with profiling enabled or to make it available explicitly: see https://hackage.haskell.org/package/base-4.9.1.0/docs/GHC-Stack.html and https://wiki.haskell.org/Debugging. And both of these options appeared in relatively recent GHC versions (and work on improving them is ongoing).
Is there a good reason to run the typechecker first? It would seem that the typechecker would be vastly simpler if it ran on a smaller syntax, especially because with the current system every syntax extension needs to touch the typechecker. This question applies especially to arrow syntax, the typechecking of which as described in comments here is known to be bogus.
I imagine one reason for this would be not emitting errors that mention generated code, but this situation is already covered in cases where a deriving clause fails to typecheck; GHC knows that code was generated.
There is a section on this question in the GHC article found in volume 2 of the book "The Architecture of Open Source Application":
Type Checking the Source Language
One interesting design decision is
whether type checking should be done before or after desugaring. The
trade-offs are these:
Type checking before desugaring means that the type checker must deal
directly with Haskell's very large syntax, so the type checker has
many cases to consider. If we desugared into (an untyped variant of)
Core first, one might hope that the type checker would become much
smaller.
On the other hand, type checking after desugaring would
impose a significant new obligation: that desugaring does not affect
which programs are type-correct. After all, desugaring implies a
deliberate loss of information. It is probably the case that in 95% of
the cases there is no problem, but any problem here would force some
compromise in the design of Core to preserve some extra information.
Most seriously of all, type checking a desugared program would make it
much harder to report errors that relate to the original program text,
and not to its (sometimes elaborate) desugared version.
Most compilers type check after desugaring, but for GHC we made the opposite choice:
we type check the full original Haskell syntax, and then desugar the
result. It sounds as if adding a new syntactic construct might be
complicated, but (following the French school) we have structured the
type inference engine in a way that makes it easy. Type inference is
split into two parts:
Constraint generation: walk over the source syntax tree, generating a
collection of type constraints. This step deals with the full syntax
of Haskell, but it is very straightforward code, and it is easy to add
new cases.
Constraint solving: solve the gathered constraints. This is
where the subtlety of the type inference engine lies, but it is
independent of the source language syntax, and would be the same for a
much smaller or much larger language.
On the whole, the
type-check-before-desugar design choice has turned out to be a big
win. Yes, it adds lines of code to the type checker, but they are
simple lines. It avoids giving two conflicting roles to the same data
type, and makes the type inference engine less complex, and easier to
modify. Moreover, GHC's type error messages are pretty good.
Compiled with ghc --make, these two programs produce the exact same binaries:
-- id1a.hs
main = print (id' 'a')
id' :: a -> a
id' x = x
-- id1b.hs
main = print (id' 'a')
id' :: Char -> Char
id' x = x
Is this just because of how trivial/contrived my example is, or does this
hold true as programs get more complex?
Also, is there any good reason to avoid making my types as general as
possible? I usually try keep specifics out where I don't need them, but
I am not extremely familiar with the effects of this on compiled languages,
especially Haskell/GHC.
Side Note:
I seem to recall a recent SO question where the answer was to make a type more
specific in order to improve some performance issue, though I cannot find it
now, so I may have imagined it.
Edit:
I understand from a usability / composability standpoint that more general is always better, I'm more interested in the effects this has on the compiled code. Is it possible for me to be too eager in abstracting my code? Or is this usually not a problem in Haskell?
I would go and make everything as general as possible. If you run into performance issues you can start thinking about messing with concrete implementations but IMHO this will not be a problem very often and if this really gets an problem then maybe your performance need will be as great as to think about moving into imperative-land again ;)
Is there any good reason to avoid making my types as general as possible?
No, as long as you have the Specialize pragma at your disposal for those rare situations where it might actually matter.
Is this just because of how trivial/contrived my example is
Yes. Namely, try splitting the definition of id' and main into different modules and you should see a difference.
However, Carsten is right: there may be performance-related reasons to use concrete types, but you should generally start with general types and use concrete implementations only if you actually have a problem.
General types usually make your functions more usable, in my opinion.
This may be a poor example, but if you're writing a function such as elem (takes a list and an element and returns true if the list contains that element and false otherwise), using specific types will constrain the usability of your function. ie. if you specify the type as Int, you can't use that function to check if a String contains a certain character, for example.
I'm not quite sure about performance, but I haven't experienced any issues and I use general types almost all the time.
Ive come across some answers (here in SO) saying that Haskell has many "dark corners" in its type system, and also some messy holes. Could someone elaborate on this?
Thanks in advance
I guess I should answer this, especially since two people so far have misinterpreted my remarks...
Regarding non-termination, the remark in question was slight hyperbole for dramatic effect, and referred to non-termination at the value level. This was in context of comparing Haskell to theorem provers, in an answer to someone who mentioned type-enforced correctness properties as something they particularly appreciated. In that sense, the presence of ⊥ inhabiting otherwise empty types is a "flaw", because it changes the meaning of a type like A -> B from "given an A, produces a B" into "given an A, either produces a B or crashes the program" which is, for obvious reasons, somewhat less satisfying from a proof-of-correctness standpoint.
It's also completely irrelevant to almost all day-to-day programming and no worse than any other general-purpose language because, of course, the possibility of non-termination is required for Turing-completeness.
I don't have any problem with UndecidableInstances. Actually, it bothers me less than ⊥ at the value level does because it only crashes GHC when compiling, not the finished program. OverlappingInstances is another matter, though, and the ad hoc mishmash of GHC extensions to provide little bits of things that would most naturally require dependent types certainly qualifies as "messy".
But keep in mind that most of the things I'm complaining about in Haskell are only a problem because of the otherwise very solid foundation. Most type systems in other statically typed languages aren't even coherent enough to be called "wrong" in comparison, and cleaning up the stuff I'm calling "messy" is an active and ongoing area of research.
Haskell's type system has no problems or messy holes, actually. Haskell 98 can be fully type-inferred. It possesses what is known as the "principal type" property, which is to say that any given expression has at most a single most general type. There are, however, a range of expressions which are good and useful and valid but do not type under Haskell 98. Most important of these are higher-ranked types. forall a b. (a -> b) -> a -> b is an (uninteresting) example of a rank-one type, which is to say that the forall is only at the very outside. forall b. -> (forall a. a -> a) -> b -> b is an example of a useless but possible type which is not rank-one, and cannot be expressed in Haskell98. Higher ranked types are one of many things which break the principal type property.
As one adds more and more extensions to the basic Haskell98 system, there begin to be tradeoffs introduced between the ability to write really powerful types which express both different types of polymorphism and different types of constraints and the ability to have as much code as possible completely type-inferred. At the very edges of what's possible, types can get messy and complicated, and occasionally you can run into things that seem like they should work but don't. But at that point, you're generally doing what's known as "type-level programming" where a great deal of your application logic has been embedded in the types themselves, and through a combination of typeclass tricks you've conned the compiler into, essentially, running the types as a program at compile time.
I disagree, by the way, with camccann's assertion that potential nontermination is a messy compromise in the type-checker. I think it's a perfectly useful feature, in fact a prerequisite for turning-completeness at the type level, and you only risk it if you explicitly ask the compiler to start allowing lots of dodgy stuff.
So you're referring to Camccann saying "Haskell's type system is full of holes, due to nontermination and other messy compromises"? I think he's talking about the UndecidableInstances extension and probably a few others.
Then you referred to Norman, I can only assume, saying "Haskell's type system is ambitious and powerful, but it is continually being improved, which means there is some inconsistency as a result of history.". I'm sure he had something in mind but will let him clarify when he see`s this question.