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.
> 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 evaluatedPretty 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.
https://www.haskell.org/hoogle/?hoogle=a+-%3E+[b]
It's as useful as you might hope!
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
* `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.
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:
tt :: Show a => a -> a
tt x = trace (show x) x
So you surround your expressions with it and see there values, when those values get evaluated of course. Eg: myParser = (\_ code -> tt code) <$> parseComment <*> parseCode myfun a b | trace ("myfun " ++ show a ++ " " ++ show b) False = undefined
myfun a b = ...
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].
./app +RTS -xc
> 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 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.
(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)
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!