I am a big fan of strongly typed languages, and my favorite GC’d language is Haskell. And I want you, the reader, to keep that in mind today. What I am writing is some commentary about a language I deeply love, some loving criticism.
So here’s what happened: A few days ago, I was showing off some Haskell for a friend who primarily programs in Python. The stakes were high – could I demonstrate that this strange language was worth some investigation?
My primary focus was on infinite lists, and defining fibonacci
as a recursive data structure – all fun things to show off Haskell’s laziness. But at some point, we wrote an expression by accident that had a type error in it, and so we got to see how the compiler treated such things. I don’t remember the exact expression – it was deep in context – but the problem was I was trying to add an integer to an list. Something analogous to 1+[2,3]
.
Now, in some “weakly typed” languages, this sort of thing is actually allowed, as a colleague of mine recently pointed out:
[jim@palatinate:~]$ node
> 1+[2,3]
'12,3'
This is, of course, hilarious. But! We shouldn’t paint “weakly typed” languages with such a broad brush. In my friend’s native Python, it would have been an error, as it should be. It is a run-time error, but what does that matter when you’re working in an interpreted language, writing ad hoc scripts. The important thing is that failure is recognized as failure, and it doesn’t try to continue with nonsense:
[jim@palatinate:~]$ python3
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1+[2,3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'
This is an error message. It’s even a pretty decent error message. There are many things you can pass to the +
operator in Python, but an int
and a list
together are not among them.
So now, what did Haskell do, this language that I’m trying to show off? Well, unfortunately, my friend didn’t see the actual problem in the code, but was first made aware of it from the compiler’s error message. And if you’ve ever done this before in Haskell, you’re probably wincing right now, because you know what this error message is:
[jim@palatinate:~]$ ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
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]
Now, my friend didn’t understand this error message at all. Since I was in Demonstration Mode, my instinct was to explain it to him, but after a few false starts, I realized that this would simply not help, and pointed out that you couldn’t add integers to lists, and showed him where this was happening (it was a little more subtle than this example).
But since then, my colleagues and I were discussing error messages in Slack, specifically how good Rust’s error messages are, specifically how much better they are than Haskell’s. So I had an opportunity to paste that very bad Haskell error message me and my friend discovered into the Slack. There, it served as a case study, so we could discuss how problematically incomprehensible it is, sparking a lot of discussion, from which I shall try to extract the most interesting parts into this post.
For one, this error message has little to do with the concrete problem. The problem is – and the error message should say this – that you can’t add lists. Specifically, in Haskell, you can only add things that implement the Num
typeclass (which lists don’t), and so you’d think the compiler would be smart enough to mention anywhere in this error message something along the lines of “expecting [a]
to have Num
instance, but it does not.” That’s the actual problem, even if not well-explained.
But instead, ghc
tries to assume you meant what you wrote, and figure out a way in which [a]
can have the Num
instance. This is where it fails, and then it gives advice on how to make that succeed. As my professor-colleague points out, this is dangerous advice, especially for beginners, because there’s no way that using FlexibleContexts
will actually help in that situation. The problem isn’t that these lists aren’t numbers in particular, and that you need to only accept lists that are numbers in your function. The problem is that no lists are (or at least should be) numbers! But a beginner might just follow the advice, try to figure out what the hell FlexibleContexts
are, and find themselves in a world of pain, and no closer to solving the actual problem.
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. Given that Haskell (like Rust) has return-type polymorphism, it can directly express this in the type system:
Prelude> :type 1
1 :: Num p => p
In Rust, this would be something like impl Num
. It means that 1
can be any type that is Num
. Combine that with the fact that +
requires its arguments to be Num
and to match ((+) :: Num a => a -> a -> a
), and when we see 1+[2,3]
, we’re simply left trying to figure out how [2,3]
is Num
.
If we did not have this polymorphic literal, this notion that the meaning of 1
is flexible, we would have seen a much more comprehensible error message. If 1
meant the same thing as (1::Integer)
(or any arbitrary choice), we’d have this beautiful explanation:
Prelude> (1::Integer) + [2,3]
<interactive>:4:16: error:
• Couldn't match expected type ‘Integer’
with actual type ‘[Integer]’
• In the second argument of ‘(+)’, namely ‘[2, 3]’
In the expression: (1 :: Integer) + [2, 3]
In an equation for ‘it’: it = (1 :: Integer) + [2, 3]
Or even if we just had non-numbers on both sides, we’d similarly have a better error message:
[jim@palatinate:~]$ ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> () + [1,2]
<interactive>:1:6: error:
• Couldn't match expected type ‘()’ with actual type ‘[Integer]’
• In the second argument of ‘(+)’, namely ‘[1, 2]’
In the expression: () + [1, 2]
In an equation for ‘it’: it = () + [1, 2]
Prelude>
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. Rust as a community puts a lot of energy into good error messages, so that even though Rust also has a trait you could add to arrays to make +
work, it still has a better error message:
error[E0277]: cannot add `[{integer}; 2]` to `{integer}`
--> test.rs:2:7
|
2 | 1 + [2,3];
| ^ no implementation for `{integer} + [{integer}; 2]`
|
= help: the trait `Add<[{integer}; 2]>` is not implemented for `{integer}`
But I also think the semantics of 1
are too liberal, leaving the compiler in an awkward place. See, the weird thing is, you can declare [2,3]
a number, making 1+[2,3]
an expression that adds two lists:
instance Num [a] where
(+) = (<>)
(-) = (<>) -- Eh, why not?
(*) = (<>)
negate = reverse
abs = id
signum = const []
fromInteger i = take (fromInteger i) $ repeat undefined
main = do
print $ signum $ 1 + [2,3]
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.
And in the end, this leaves me with the feeling that Haskell has this in common with Javascript, and that makes me sad. A polymorphic enough strongly typed language is no longer strongly typed.
from Hacker News https://ift.tt/ziYraW3
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.