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.
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.
In the Haskell community, we are slowly adding features of dependent types. Dependent types is an advanced typing feature by which types can depend on values. Some languages like Agda and Idris already have them. It appears to be a very advanced feature requiring an advanced type system, until you realize that python has had dependent types has had the dynamic typing version of dependent types, which may or may not be actual dependent types, from the beginning.
For most any program in a functional programming language, there is a way to reperesent it as an untyped lambda calculus term, no matter how advanced the typing. That's because typing only eliminates programs, not enable new ones.
Strong Typing wins us safety. How classes of errors that happened at run time can no longer happen at run time. This safety is rather nice. Besides this safety though, what does strong typing give you?
Are there an additional benefits of a strong type system besides safety?
(Note that I'm not saying that strong typing is worthless. Safety is a huge benefit in and of itself. I'm just wondering if there are additional benefits.)
First, we need to talk a bit about the history of the simply typed lambda calculus.
There are two historical developments of the simply typed lambda calculus.
When Alonzo Church described the lambda calculus the types were baked in as part of the meaning / operational behavior of the terms.
When Haskell Curry described the lambda calculus the types were annotations put on the terms.
So we have the lambda calculus a la Church and the lambda calculus a la Curry. See https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus#Intrinsic_vs._extrinsic_interpretations for more.
Ironically, the language Haskell, which is named after Curry is based on a lambda calculus a la Church!
What this means is the types aren't simply annotations that rule out bad programs for you. They can "do stuff" too. Such types don't erase without leaving residue.
This shows up in Haskell's notion of type classes, which are really why Haskell is a language a la Church.
In Haskell, when I make a function
sort :: Ord a => [a] -> [a]
We're passing an object or dictionary for Ord a as the first argument.
But you aren't forced to plumb that argument around yourself in the code, it is the job of the compiler to build that up and use it.
instance Ord Char
instance Ord Int
instance Ord a => Ord [a]
So if you go and use sort on a list of strings, which are themselves lists of chars, then this will build up the dictionary by passing the Ord Char instance through the instance for Ord a => Ord [a] to get Ord [Char], which is the same as Ord String, then you can sort a list of strings.
Calling sort above, is a lot less verbose than manually building a LexicographicComparator<List<Char>> by passing it an IComparator<Char> to its constructor and calling the function with an extra second argument, if I were to compare the complexity of calling such a sort function in Haskell to calling it in C# or Java.
This shows us that programming with types can be significantly less verbose, because mechanisms like implicits and typeclasses can infer a large part of the code for your program during type checking.
On a simpler basis, even the sizes of arguments can depend on types, unless you want to pay fairly massive costs for boxing everything in your language up so that it has a homogeneous representation.
This shows us that programming with types can be significantly more efficient, because it can use dedicated representations, rather than paying for boxed structures everywhere in your code. An int can't just be a machine integer, because it has to somehow look like everything else in the system. If you're willing to give up an order of magnitude or more worth of performance at runtime, then this may not matter to you.
Finally, once we have types "doing stuff" for us, it is often beneficial to consider the refactoring benefits that mere safety provides.
If I refactor the smaller set of code that remains, it'll rewrite all that type-class plumbing for me. It'll figure out the new ways it can rewrite the code to unbox more arguments. I'm not stuck elaborating all of this stuff by hand, I can leave these mundane tasks to the type-checker.
But even when I do change the types, I can move arguments around fairly willy-nilly, comfortable that the compiler will very likely catch my errors. Types give you "free theorems" which are like unit tests for whole classes of such errors.
On the other hand, once I lock down an API in a language like Python I'm deathly afraid of changing it, because it'll silently break at runtime for all my downstream dependencies! This leads to baroque APIs that lean heavily on easily bit-rotted keyword-arguments, and the API of something that evolves over time rarely resembles what you'd build out of the box if you had it to do over again. Consequently, even the mere safety concern has long-term impact in API design once you ever want people to build on top of your work, rather than simply replace it when it gets too unwieldy.
That's because typing only eliminates programs, not enable new ones.
This is not a correct statement. Type-classes make it possible to generate parts of your program from type-level information.
Consider two expressions:
readMaybe "15" :: Maybe Integer
readMaybe "15" :: Maybe Bool
Here I'm using the readMaybe function from the Text.Read module. At term level those expressions are identical, only their type annotations are different. However, the results they produce at runtime differ (Just 15 in the first case, Nothing in the second case).
This is because the compiler generates code for you from the static type information you have. To be more precise, it selects a suitable type class instance and passes its dictionary to the polymorphic function (readMaybe in our case).
This example is simple, but there are way more complex use cases. Using the mtl library you can write computations that run in different computational contexts (aka Monads). The compiler will automatically insert a lot of code that manages the computational contexts. In a dynamically typed language, you would have no static information to make this possible.
As you can see, static typing not only cuts off incorrect programs but also writes correct ones for you.
You need "safety" when you already know what and how you want to write. It's a very small part of what types are useful for. The most important thing about types is that they make your reasoning structured. When someone writes in Python a + b he doesn't see a and b as some abstract variables — he sees them as some numbers. Types are already there in the internal language of humans, Python just doesn't have a type system to talk about them. The actual question in the "typed vs untyped (unityped) programming" dispute is "do we want to reflect our internal structured concepts in a safe and explicit or unsafe and implicit way?". Types don't introduce new concepts — it's untyped reasoning forgets the existing ones.
When someone looks at a tree (I mean a real green one) he doesn't see every single leaf on it, but he doesn't treat it as an abstract nameless object as well. "A tree" — is an approximation that is good enough for most cases and that's why we have Hindley-Milner type systems, but sometimes you want to talk about a specific tree and you do want to look at leaves. And that's what dependent types give you: the ability to zoom. "A tree without leaves", "a tree in the forest", "a tree of a particular form"... Dependently typed programming is just another step towards how humans think.
On a less abstract note, I have a type checker for a toy dependently typed language, where all typing rules are expressed as constructors of a data type. You don't need to dive into the type checking procedure to understand the rules of the system. That's the power of "zooming": you can introduce as complex invariants as you want, thus distinguishing essential parts from not important ones.
Another example of the power dependent types give you is various forms of reflection. Look e.g. at the Pierre-Évariste Dagand thesis, which proves that
generic programming is just programming
And of course types are hints, many functions and abstractions I defined I would define in a far more clumsy way in a weakly typed language, but types suggested better alternatives.
There is just no question "What to choose: simple types or dependent types?". Dependent types are always better and they of course subsume simple types. The question is "What to choose: no types or dependent types?", but that question doesn't stand for me.
Refactoring. By having a strong type system you can safely refactor code and have the compiler tell you whether what you are doing now even makes sense. The stronger the typing system, the more refactor errors are avoided. This of course means your code is a lot more maintainable.
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.
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.
Do the compiler or the more "native" parts of the libraries (IO or functions that have access to black magic and the implementation) make assumptions about these laws? Will breaking them cause the impossible to happen?
Or do they just express a programming pattern -- ie, the only person you'll annoy by breaking them are people who use your code and didn't expect you to be so careless?
The monad laws are simply additional rules that instances are expected to follow, beyond what can be expressed in the type system. Insofar as Monad expresses a programming pattern, the laws are part of that pattern. Such laws apply to other type classes as well: Monoid has very similar rules to Monad, and it's generally expected that instances of Eq will follow the rules expected for an equality relation, among other examples.
Because these laws are in some sense "part of" the type class, it should be reasonable for other code to expect they will hold, and act accordingly. Misbehaving instances may thus violate assumptions made by client code's logic, resulting in bugs, the blame for which is properly placed at the instance, not the code using it.
In short, "breaking the monad laws" should generally be read as "writing buggy code".
I'll illustrate this point with an example involving another type class, modified from one given by Daniel Fischer on the haskell-cafe mailing list. It is (hopefully) well known that the standard libraries include some misbehaving instances, namely Eq and Ord for floating point types. The misbehavior occurs, as you might guess, when NaN is involved. Consider the following data structure:
> let x = fromList [0, -1, 0/0, -5, -6, -3] :: Set Float
Where 0/0 produces a NaN, which violates the assumptions about Ord instances made by Data.Set.Set. Does this Set contain 0?
> member 0 x
True
Yes, of course it does, it's right there in plain sight! Now, we insert a value into the Set:
> let x' = insert (0/0) x
This Set still contains 0, right? We didn't remove anything, after all.
> member 0 x'
False
...oh. Oh, dear.
The compiler doesn't make any assumptions about the laws, however, if your instance does not obey the laws, it will not behave like a monad -- it will do strange things and otherwise appear to your users to not work correctly (e.g. dropping values, or evaluating things in the wrong order).
Also, refactorings your users might make assuming the monad laws hold will obviously not be sound.
For people working in more "mainstream" languages, this would be like implementing an interface, but doing so incorrectly. For example, imagine you're using a framework that offers an IShape interface, and you implement it. However, your implementation of the draw() method doesn't draw at all, but instead merely instantiates 1000 more instances of your class.
The framework would try to use your IShape and do reasonable things with it, and God knows what would happen. It'd be kind of an interesting train wreck to watch.
If you say you're a Monad, you're "declaring" that you adhere to its contract and laws. Other code will believe your declaration and act accordingly. Since you lied, things will go wrong in unforeseen ways.