Sure, the error that talks about FlexibleContexts could definitely be improved, but if you do enable that extension, then you get a much better error:
• No instance for (Num [Integer]) arising from a use of ‘it’
This one can be more straightforwardly understood to mean "you can't use a list of numbers as if it were a number". Also, starting in GHC 9.2, FlexibleContexts is enabled by default, so you'll get that better error the first time.
Compare this to the error message you get from Rust with the same program though:
error[E0277]: cannot add `[{integer}; 2]` to `{integer}`
--> src/main.rs:2:7
|
2 | 1 + [2, 3]
| ^ no implementation for `{integer} + [{integer}; 2]`
|
= help: the trait `Add<[{integer}; 2]>` is not implemented for `{integer}`
For more information about this error, try `rustc --explain E0277`.
Good error messages are important for adoption of languages with strong type systems. The complexity is already foreign enough for people who aren't familiar with type theory.
Rust is more constrained in ways that make type inference easier and less ambiguous. One issue mentioned in the OP is the way numeric literals are typed in Haskell. Haskell also has a lot of optional language extensions, some of them quite researchy, and that complicates type inference and error messages. It also has curried functions.
These can all be fantastic things to have, but they involve tradeoffs. Those tradeoffs mostly make sense in the context of Haskell's original research focus. "Adoption" of the kind you're presumably referring to was an explicit non-goal for a long time, and still is among some users.
Edit: sometime else pointed out that more recent Haskell compilers give the error "No instance for (Num [Integer]) arising from a use of `it'" for this example, so this whole post may be kinda BS.
The funny thing is, when it comes to books and tutorials and such, there's way more material available at the beginner's level than at the intermediate or beyond. Seems like after a certain point you're expected to understand everything from looking at some type signatures.
That error's a bit better, but not much. You basically have to know that `+` only works on stuff that implements `Num` or else it still makes no sense. Compare that to the Rust/Python errors which are so much easier to understand.
While I agree that this error message sucks, I would like to point out that it could be improved greatly by adding a type annotation.
Prelude> :{
Prelude| result :: Int
Prelude| result = 1 + [1,2]
Prelude| :}
<interactive>:3:10: error:
• Couldn't match expected type ‘Int’ with actual type ‘[a0]’
• In the expression: 1 + [1, 2]
In an equation for ‘result’: result = 1 + [1, 2]
Now I agree that this might not be an ideal thing to do when showing someone Haskell on a REPL, but I will argue in favor of annotating types at least for top-level declarations in a file. Especially for beginners, as learning to follow the types I feel is helpful to understanding the language and, in turn, its error messages.
I am told that well before I learned Haskell, type inference was the Cool Thing. However, I am of the opinion that a strong type system like Haskell's is best used with guidance (annotations) from the programmer. The more powerful your types, the better a specification you can write. And with a better specification, the type checker can give you better insight into what's wrong when it gets stuck.
> Once you’ve defined lists as a number, 1 is suddenly a list if it wants to be. And this contributes to the difficulty of finding the right error message: what you asked for is possible after all.
The key part is more expressive power makes good error messages harder.
Maybe Rust is right to make + overloadable, but not literals, which is how it makes the better error (as described in the piece), but I am not sure. Perhaps someone uses Complex number or Fixed Point literals a lot can chime in.
Racket's teaching languages are the wisest of all. Beginners and experts want different things, and there is no good reason we have our cake and eat it too, catering to their needs separately while allowing full code interop.
Everyone, please, for the love of God, stop saying that good error messages benefit beginners only.
They benefit everyone.
There are only so many things that can fit into a human's head, and the skill required to decipher indecipherable error messages end up replacing some other skill.
And don't forget, these error messages will end up in the logs where you'll have to spend time figuring out what the hell happened.
Edit: typos from iPhone's increasingly unusable keyboard
GHC error messages and needless category theory and algebra jargon are the two worst aspects of programming in Haskell. You eventually get used to both of them and, once you do, you become even more productive. Everything else about the language is magnificent....until you need to improve performance hotspots.
That's great. It's been rare for me too, but I'm currently trying to solve an issue where it seems like we've inadvertently built up some kind of mega thunk that's making performance absolutely horrible for large inputs. The hotspot in question currently implemented as big recursive loop, so it's proving hard to untangle. Normally shortcut fusion and laziness make it a non-issue but it seems like we hit a thunk chain here or something.
It’s too bad that the humorless are downvoting us both.
In any event wolf-fencing very much appears to be Haskell specific based on some web searching. Or perhaps it’s a home improvement product? Several search results appear to indicate that[1].
It's universally applicable. I've done it in C, Scala, Go, Java, Haskell..everything.
Perf-debugging wise - you basically remove parts of your computation and perform binary search to hone in on the parts that are on the critical path.
I agree that a lot of Haskellers come off as "what's the problem?" (including myself). But Haskell is a small, niche, weird language. You kinda need that attitude to dive deep into it.
I claim that "wolf-fencing" appears to be Haskell-specific for the same reason that "monad" does a good job of appearing to be Haskell-specific. It's because the Haskell community is uniquely very ready to adopt terms which are widely used in mathematics. (It's "lion hunting" in maths, but obviously from the same root.)
They're bad. I've only compiled one small Haskell program and even after stripping had to resort to upx to compress the binary and make it easy to distribute.
$ ghci
GHCi, version 9.2.1: https://www.haskell.org/ghc/ :? for help
Main λ> 1+[2,3]
<interactive>:1:1: error:
• No instance for (Num [Integer]) arising from a use of ‘it’
• In the first argument of ‘ghciInteractivePrint’, namely ‘it’
In a stmt of an interactive GHCi command: ghciInteractivePrint it
As a Haskell beginner I have no idea what this error message means.
What is `it`? Perhaps it is the argument for ghciInteractivePrint? That is what the error says. But what is ghciInteractivePrint that wasn't in the original code? I'm guessing an alias for print.
The error seems to be
No instance for (Num [Integer])
Which doesn't say anything about +
So I'm still in the dark about this error message!
If you presume the user knows the type of (+) which I think is kind of fair.
Ideally though it would be:
(+) has no instance for (Num [Integer])
The the improvement the GHC team made is already huge for those that have at least heard of Num, which I'd guess is anyone who's went through a Haskell tutorial.
> What is my take-away here? I don’t think the compiler has been sufficiently tweaked when it comes to error messages, or that the Haskell community cares sufficiently about beginners.
I don't think it's fair to draw the conclusion that the Haskell community don't care sufficiently about beginners from this, since only selected few of the community has the ability (and time) to work on GHC and improve the state of error messages.
I was going to say the same thing. While it may be true that beginners are not sufficiently appreciated or that error quality is not sufficiently prioritized, these extrapolations do seem a little rude in the context of what is effectively a defect report to a volunteer project.
> Part of what causes this is the type of 1 itself. Haskell, unlike Rust, allows literals like 1 to be interpreted in any number type.
This is false. Rust allows 1 to be a u8, an i16, etc. It does distinguish between integral and floating numeric literals, but integer literals are still polymorphic. The difference is just that in Rust, numeric literals are builtin rather than extensible.
Rust does not treat integer types as interchangeable.
Rust integers have a type (width and signedness) and you can't combine them without explicitly handling for overflow, etc. Any polymorphism through traits (eg. std::ops::Add) typically only operates over the same type as the source integer.
If types are left unspecified, it's only because type inference knows what type you mean. Since integers are frequently used in structs and function params (places where Rust requires typing), Rust knows the type when you perform operations on these variables. Outside of these cases, Rust will generally require you to disambiguate.
Ehh, that's a bit overblown. In particular, even if you actually did make the weird list-number, you'd end up with a type error as soon as you tried to use it somewhere expecting a regular number.
That would let you make the instance only be a little bit unlawful instead of totally unlawful, but I don't see how it would affect the type errors you'd get.
I've worked quite a bit with OCaml and one of the first stumbling blocks was its strictness when it comes to number literals and even operators. In OCaml, 1 + 1.0 is not well-typed. Surprisingly, even the lack of overloading doesn't hurt too much in practice, IMO. In essence, I think that Haskell went to far with its polymorphic literals, even though it's a nice idea at first glance.
In OCaml, 1.0 + 1.0 isn't even well-typed. You don't need to mix operand types to run into walls with OCaml. The lack of operator overloading may not hurt much in practice, but doesn't scream "elegant solution" to me.
Haskell doesn't overload its operators either (in the sense of there being multiple definitions with different type signatures); instead, it has a working type hierarchy that allows the same arithmetic operators to be used for all numeric types (through Num->Integral, Num->Real, Num->Rational and even Num->RealFloat->Complex). (1::Int) + 1.0 isn't well-typed in Haskell either, but at least (a + b) is a valid operation for all numeric data types. And, most importantly, as a developer you can define your own types that also implement these same operators.
OCaml does have polymorphic literals too: "%s" can be both type string and type formatter. It's just very limited and requires built-in compiler support, so it's not something you as developer can take advantage of in your own code. Although in practice I use OCaml more than Haskell, there's many times in OCaml where I wish it had more of Haskell's expressive type system.
The errors used to be somewhat better in the past, but more features have been added to the language, type system and std libs. Those features required the core parts to be slightly more expressive and generic, resulting in more abstract error messages.
Not great, but I think there are definitely no easy fixes here.
But the error that this article complains about is from a compiler that's 3 years old and 4 major releases out of date, and doing the exact same thing on the latest version will give you a better error.
To further elaborate, this is the error you get on the latest GHC version (9.2.1):
GHCi, version 9.2.1: https://www.haskell.org/ghc/ :? for help
ghci> 1 + [2, 3]
<interactive>:1:1: error:
* No instance for (Num [Integer]) arising from a use of `it'
* In the first argument of `print', namely `it'
In a stmt of an interactive GHCi command: print it
Try explaining semiconductors to an Ancient Roman - they'd have exactly the same objection! You can't understand the words until you understand the objects they're describing, and you can't get that understanding just by repeatedly asking for an explanation that uses words you already know.
Rust is strongly typed, it won't let you add or compare two values of different datatypes, even if they're both integers. For example you can't add a `u8`(byte) and a `i64`(long int) without casting.
Meanwhile Python, JS and many other weakly typed languages will happily add an integer and a float, maybe even a string and a number without complaints.
The advantage of the former are more rigorous compile-time checks, which might catch bugs where you added/compared the wrong values. But of course it's a bit more cumbersome especially if you're not used to it. I personally prefer it, I've had numerous cases where my Python script crashed 10 minutes into some data processing because it realised I tried to add two incompatible values.
> Meanwhile Python, JS and many other weakly typed languages will happily add an integer and a float, maybe even a string and a number without complaints.
Actually, that's a type error in Python, hence my question.
Yes, but that's mostly an exception compared to the rest of Python's type system. Python doesn't care a lot about types in general.
Sure, it's not quite as lenient as JS, but JS is a pretty extreme case - you won't find many other languages that'll happily do an addition of an object and an array.
The weak/strong classsification is a bit less well-defined than the dynamic/static classification, but in general, languages differ in how strongly they 'care' about their types.
For example, in Javascript, you can add anything to anything, with rules that define which automatic conversions will be performed. Sometimes, hilarity ensues. In contrast, a strong language would require explicit conversions and throw an error otherwise.
In languages with ad-hoc polymorphism and operator overloading, you can emulate the Javascript experience by manually adding all the different cases yourself, in some sense 'weakening' the type system.
> In contrast, a strong language would require explicit conversions and throw an error otherwise.
That's not the definition the author is going with considering that they're saying that Python (which requires explicit type conversions in most cases) is weakly typed.
From the replies here I'm seeing that there's no clear definition of "weakly" and "strongly" typed languages. I don't think it makes a lot of sense to use these terms.
That's not the definition the author is going with
I think it is, given that he links to https://xkcd.com/1537/ as stereotypical 'weak' behaviour.
Python sits somewhere inbetween Javascript and Haskell: For example, Haskell does not allow adding floats and integers, which Python will happily do despite a potential loss of precision.
No, those aren't the same thing. For example, C is both statically typed (there's no type checks or type information at runtime) and weakly typed (if you use a value of one type, like char, somewhere that another type, like _Bool, was expected, an implicit conversion will usually happen instead of an error).
What I find curious about the error message is that it tries to tell you what is the cause of the error, but it does not actually show you source-code that causes it.
Wouldn't it be much easier to understand what is causing the error if I simply saw a short section of the source-code where I could FIX the error, by modifying something in the shown section?
What is the line-number where the error occurs? Show me the code, and where it is in relation to rest of the code.
Yeah, that's definitely a confusing part of GHCi. If you see an error message like the one in the post
Prelude> 1+[2,3]
<interactive>:1:1: error:
• Non type-variable argument in the constraint: Num [a]
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall a. (Num a, Num [a]) => [a]
The "line-number" is found on this line
<interactive>:1:1: error:
Since it's in a REPL, it is referring to the first line you entered.
The more confusing part is that the important error is found here
• When checking the inferred type
it :: forall a. (Num a, Num [a]) => [a]
where it is assumed that you know all REPL statements that don't bind to a variable are bound to a variable named "it". So the error is really saying that the whole expression 1+[2,3] has that inferred type, and that type is bad.
If you load your example into a file and run GHC on it, it will produce a still confusing error message, but it will at least show you where the problems are
error-message.hs:3:11: error:
• No instance for (Num [a0]) arising from a use of ‘+’
• In the expression: 1 + [1, 2]
In an equation for ‘badType’: badType = 1 + [1, 2]
|
3 | badType = 1 + [1,2]
| ^^^^^^^^^
(I've omitted another, much longer error message it spewed out which is similarly confusing)
Some advanced Haskell features make this hard because constraints like 'a is a number' are solved separately. For instance
data Tag a where
IsInt :: Tag Int
IsDouble :: Tag Double
someNum :: Num a => a
someNum = 3 * 4 + 5
pickNum :: Tag a -> a
pickNum IsInt = someNum
pickNum IsDouble = someNum * 2.5
Constraints can be delayed and changed by pattern matching, so the origin might not be obvious. There are ways around this, but they require tracking why values are set and to propagate this information around. Haskell has Turing complete type level functions so you have to figure out how they propagate this dependency information.
GHC handles some easy cases, if you compiler in a file it does point to `+`, but it can get hairy. Also, currying can make it harder to say 'you swapped to arguments'
GHC does do those things when you're compiling code in files. The only reason you aren't seeing it in this article is that these errors came from entering one expression at a time into the REPL.
That has been my constant experience in Haskell. When the compiler complaints are gibberish, add types until it makes sense. It leads to this culture where people say that you have to nail down all the types. Which certainly is good advice for beginners. Still I'm sad about this because it easily doubles code with type chatter.