When it works, it's magical. Finally realising that monads allow little bubbles of cause-and-effect. STM, which is utterly magical. Realising that not having side effects is freeing, not restricting. Collapsing huge chunks of code into little bits of pattern matching. Finding myself casually using a higher-order type because it was the easiest way to solve a problem.
When it's warty, it's really warty. Namespaces... oh god, namespaces. About three different rather poor ways to handle exceptions. A billion little unmemorable functions with symbolic names. Total lack of debugging tools. Trying to find where an a out-of-bounds list error was happening. Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
Some of the warts are just embarrassing, and need fixing ASAP. Lack of information on a runtime exception is the biggest. Even BASIC would at least give me a line number!
Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
This is a completely wrong assumption. The point of laziness is not so you won't have to care about evaluation order. Everyone would like to have a language in which you don't have to think about evaluation order, but that has proven to be rather hard. Not because the people who invented Haskell or implemented the compilers are idiots who don't know what they're doing, but because it a hard problem to tackle. With many pros and cons on both the lazy and the strict side.
This isn't embarrassing at all. At least you need to think less about evaluation order in Haskell than in most other languages.
I think lazy evaluation is very hard, especially when you are dealing with a codebase of of say 50 files with structures containing other structures, the line between lazy and strict is very hard to see.
The difficulty learning lazy evaluation skill goes from
* 0 where you know nothing to 5 where you sort of understand.
* 5 - 90 is a space where you think you know what it is, but you really don't. You'll probably get yourself in trouble more than helping.
* 90-100 is when you grok it and can use it effectively.
I am an avid haskell user for a few years and I am still 50/50 on most of my conclusions about laziness without asking someone else.
I think it can be unexpected if you're used to other semantics, but the behavior of laziness is very well defined.
> let x = [1 .. 100] :: [Int]
> seq x () -- force the first cons
> :sprint x
x = 1 : _ -- we evaluated the first cons
> foldr (\e l -> e + l) 0 x
5050 -- printing this value forces foldr to complete
> :sprint x
x = [1,2,3,4,5...100] -- all conses evaluated
A billion little unmemorable functions with symbolic names
Pretty much. If you though Common Lisp was bad, this is much worse. And that makes for a major (re-)learning curve every time you stop programming in Haskell for a couple of months.
I actually like the symbolic function names. They're a pattern and method to them that once you learn it, it's super convenient. Perhaps it just needs more obvious documentation.
Trying to find where an a out-of-bounds list error was happening
I agree that in some real world runtime debugging, haskell falls short, due to the strictness that it imposes and how observation through debugging can change how the code executes. I have also spent a week or more tracking down a memory bug that was solved by a single `!` (to add strictness).
But the glory of haskell, is that you really don't need to debug runtime code. If you take advantage of the types
Having used haskell for about 5 years now, I think anywhere that you can even have an out-of-bounds list error is an anti-pattern. You should restructure your data to match the type.
Using list lookups aren't what lists are for. Unless you are implementing a specific algorithm, manipulation and access of lists should only be done via folds, maps, pattern matching or similar functions.
If you are accessing members of a list directly by their index, you want a different data structure
Personally, I've been spending a lot of my recent time trying to debug a Parsec parser (it seems to be going into loops of backtracking, but I can't figure out where and why), and it's been a clusterfuck.
* `trace` and `traceM` don't actually get evaluated, so I can't use printing to find my bugs.
* Even putting `trace "..." True` as a guard to my parser combinators doesn't seem to actually print anything.
* `seq` and `deepseq` require that I implement all kinds of instances for datatypes whose innards I can't actually access, so I can't force the evaluation.
* I'm getting tempted to use `unsafePerformIO` just to get some damn debug output, but can't figure out how to make it have the right type inside the Parsec monad.
Debugging Haskell sucks when you need to debug dynamic behavior rather than type errors.
You can get line numbers and even stack traces if you compile your program (assuming you use GHC) with `-prof` and execute it with `+RTS -xc`. But most libraries try their best to avoid using exceptions altogether and it's generally discouraged to use the non-total functions from Prelude like `head` or `!!` and use safer alternatives instead.
I do remember trying to use -prof, and being unable to make it work --- but it's too long ago to remember the details. I think it was something about being unable to find a prof version of one of the libraries I was linking to?
I did also try the ghci interactive debugger, and couldn't make that work either. Now, I like command line debuggers, but I found the ghci one utterly opaque. I did eventually get it to catch the exception... but then it wouldn't tell me where it happened.
In general, trying to track down the exception was an utterly miserable experience. I do understand that Haskell has special needs when it comes to order of evaluation, due to the way that thunks are unwound, which means that traditional stepping and stack frames are of little use; but this desperately needs to be better. And work out of the box.
Regarding avoiding exceptions... yeah, I'd love to, but it wasn't my code which was throwing the exception.
I loathe and detest user-mode packaging systems, so I actually installed my packages via Debian and then built my app using ghc --make. Which, BTW, works beautifully. So I never had to touch hackage or cabal. Luckily.
(1) Haskell is a compiled language, so you need to compile everything locally
(2) Libraries are unstable, because they are young and authors are too creative, so dependencies are unstable.
Maven, and whatever Python and Ruby have, can ship you everything you need to run. (Except for things that depend on C bindings, where they become just as painful as Haskell libs, for the same reasons)
Totally agree on the runtime exception issue. I feel like even Java with it's typed exceptions are better. With a Haskell function I have no idea what exceptions it will through and worse, if it's in IO, I have no idea what async exceptions it could throw.
Comments
My experience with learning Haskell was:
When it works, it's magical. Finally realising that monads allow little bubbles of cause-and-effect. STM, which is utterly magical. Realising that not having side effects is freeing, not restricting. Collapsing huge chunks of code into little bits of pattern matching. Finding myself casually using a higher-order type because it was the easiest way to solve a problem.
When it's warty, it's really warty. Namespaces... oh god, namespaces. About three different rather poor ways to handle exceptions. A billion little unmemorable functions with symbolic names. Total lack of debugging tools. Trying to find where an a out-of-bounds list error was happening. Trying to debug a memory leak caused by insufficient strictness --- why do I need to care about this? The runtime should just take care of it for me; the whole point of a lazy language is that I shouldn't have to care when evaluation happens.
Some of the warts are just embarrassing, and need fixing ASAP. Lack of information on a runtime exception is the biggest. Even BASIC would at least give me a line number!
This is a completely wrong assumption. The point of laziness is not so you won't have to care about evaluation order. Everyone would like to have a language in which you don't have to think about evaluation order, but that has proven to be rather hard. Not because the people who invented Haskell or implemented the compilers are idiots who don't know what they're doing, but because it a hard problem to tackle. With many pros and cons on both the lazy and the strict side.
This isn't embarrassing at all. At least you need to think less about evaluation order in Haskell than in most other languages.
intuition about lazy evaluation order can be very misleading
I agree with this statement.
I think lazy evaluation is very hard, especially when you are dealing with a codebase of of say 50 files with structures containing other structures, the line between lazy and strict is very hard to see.
The difficulty learning lazy evaluation skill goes from
* 0 where you know nothing to 5 where you sort of understand.
* 5 - 90 is a space where you think you know what it is, but you really don't. You'll probably get yourself in trouble more than helping.
* 90-100 is when you grok it and can use it effectively.
I am an avid haskell user for a few years and I am still 50/50 on most of my conclusions about laziness without asking someone else.
Is it fair to say that in Haskell, you have to think about evaluation order less, but when you do, it's harder?
I think it can be unexpected if you're used to other semantics, but the behavior of laziness is very well defined.
When you hit that difficult to reason about evaluation order in Haskell, it'll take that time you thought you gained.
Pretty much. If you though Common Lisp was bad, this is much worse. And that makes for a major (re-)learning curve every time you stop programming in Haskell for a couple of months.
I actually like the symbolic function names. They're a pattern and method to them that once you learn it, it's super convenient. Perhaps it just needs more obvious documentation.
It'd be nice to be able to search by type declaration
Hoogle lets you do exactly that, with fuzzy matching:
https://www.haskell.org/hoogle/?hoogle=a+-%3E+[b]
It's as useful as you might hope!
I am not a Haskeller, but you can use "!h searchterm" on Duck Duck Go.
OMG thank you, this was the thing that got me to finally permanently set my toolbar search in FF to Duck Duck Go...
That's why I like Scheme.
I agree that in some real world runtime debugging, haskell falls short, due to the strictness that it imposes and how observation through debugging can change how the code executes. I have also spent a week or more tracking down a memory bug that was solved by a single `!` (to add strictness).
But the glory of haskell, is that you really don't need to debug runtime code. If you take advantage of the types
Having used haskell for about 5 years now, I think anywhere that you can even have an out-of-bounds list error is an anti-pattern. You should restructure your data to match the type.
Using list lookups aren't what lists are for. Unless you are implementing a specific algorithm, manipulation and access of lists should only be done via folds, maps, pattern matching or similar functions.
If you are accessing members of a list directly by their index, you want a different data structure
Personally, I've been spending a lot of my recent time trying to debug a Parsec parser (it seems to be going into loops of backtracking, but I can't figure out where and why), and it's been a clusterfuck.
* `trace` and `traceM` don't actually get evaluated, so I can't use printing to find my bugs.
* Even putting `trace "..." True` as a guard to my parser combinators doesn't seem to actually print anything.
* `seq` and `deepseq` require that I implement all kinds of instances for datatypes whose innards I can't actually access, so I can't force the evaluation.
* I'm getting tempted to use `unsafePerformIO` just to get some damn debug output, but can't figure out how to make it have the right type inside the Parsec monad.
Debugging Haskell sucks when you need to debug dynamic behavior rather than type errors.
Note that trace is implemented in terms of unsafePerformIO (which is fine for debugging).
To get a trace message, you need to attach the trace to something that does get evaluated. I mostly define a somewhat simpler helper like this:
So you surround your expressions with it and see there values, when those values get evaluated of course. Eg:Note: I just looked at the docs linked in creichert's sibling post, and I noticed that Debug.Trace provides traceShow as a helper.
Using the trace trick needs to guard on False:
See here for some other helpful trace functions:https://hackage.haskell.org/package/base-4.8.1.0/docs/Debug-...
There is even a traceStack which will print a call stack if available.
The -xc runtime option does just that, with a stack trace[0].
> Total lack of debugging toolsI wouldn't exactly say there is a total lack. To name a few:
Thread Scope: https://wiki.haskell.org/ThreadScope
Event Log: https://ghc.haskell.org/trac/ghc/wiki/EventLog (capable of live monitoring)
GHCi Debugger: https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
EKG: https://hackage.haskell.org/package/ekg
I also consider type system features to be a very useful form of tooling:
Typed Holes: https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
Deferring Type Errors: https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
Partial Type Signatures: https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
[0] - Can't direct link, look for -xc here: https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
You can get line numbers and even stack traces if you compile your program (assuming you use GHC) with `-prof` and execute it with `+RTS -xc`. But most libraries try their best to avoid using exceptions altogether and it's generally discouraged to use the non-total functions from Prelude like `head` or `!!` and use safer alternatives instead.
I do remember trying to use -prof, and being unable to make it work --- but it's too long ago to remember the details. I think it was something about being unable to find a prof version of one of the libraries I was linking to?
I did also try the ghci interactive debugger, and couldn't make that work either. Now, I like command line debuggers, but I found the ghci one utterly opaque. I did eventually get it to catch the exception... but then it wouldn't tell me where it happened.
In general, trying to track down the exception was an utterly miserable experience. I do understand that Haskell has special needs when it comes to order of evaluation, due to the way that thunks are unwound, which means that traditional stepping and stack frames are of little use; but this desperately needs to be better. And work out of the box.
Regarding avoiding exceptions... yeah, I'd love to, but it wasn't my code which was throwing the exception.
And you haven't even mentioned Cabal, which is arguably the single worst thing about the Haskell ecosystem.
I loathe and detest user-mode packaging systems, so I actually installed my packages via Debian and then built my app using ghc --make. Which, BTW, works beautifully. So I never had to touch hackage or cabal. Luckily.
Cabal itself is fine. The problems are
(1) Haskell is a compiled language, so you need to compile everything locally
(2) Libraries are unstable, because they are young and authors are too creative, so dependencies are unstable.
Maven, and whatever Python and Ruby have, can ship you everything you need to run. (Except for things that depend on C bindings, where they become just as painful as Haskell libs, for the same reasons)
Totally agree on the runtime exception issue. I feel like even Java with it's typed exceptions are better. With a Haskell function I have no idea what exceptions it will through and worse, if it's in IO, I have no idea what async exceptions it could throw.