Analyzing Three Space Leaks in Haskell
neilmitchell.blogspot.com
neilmitchell.blogspot.com
The guys behind Haskell are utterly brilliant and seem to come up with profound new programming concepts on a regular basis, so I'm surprised they've not introduced fundamental features to solve or mitigate this problem. Until they do most people are going to be too scared to use Haskell for serious projects.
I think the key here is that Java has JProfiler. C++ and C have Valgrind. Haskell has a heap profiler built right into the compiler. But Haskell doesn't have any specific tools for thunk memory profiling. The discussion in the community is now around building tools and automating this process. It's not like this is a fundamental issue.
Languages that have complex threading and locking also have to have more complex tools to debug these issues (ThreadScope in Haskell, VTune in C++).
What I'm trying to do is put these issues into context. It would be easy for a Python dev with the GIL to scoff at the complexities associated with C++ or Java threading and this is akin to what many do when looking at laziness. Laziness is a feature that comes with it's own types of bugs and we simply need tools to manage and track down those bugs.
The breakthrough with Neil's post is that even with current tools we now have a way to track thunk-based space leaks in Haskell and ideas for some more automation on the way.
The problem is, to completely solve space leaks, Haskell should abandon pervasive laziness. But laziness is sold as one of the best things about Haskell, and most Haskellers do love laziness because it improves composability.
I myself hate pervasive laziness, and I'm all for removing it from the language. Unfortunately this argument will fail to convince most Haskellers because they're already sold on the bright sides of pervasive laziness.
Ok, this is all good, but from here there's one really annoying aspect of the Haskell community. They try to twist the reality to convince themselves. The typical reaction when these kinds of arguments are brought on the Haskell community is "space leaks are actually rare in the real world applications". Yes, this is the saying from the very community that emphasizes on program correctness. Sure, buffer overflow and memory leaks also rarely happen in other languages? Isn't "exact resource usage" also a part of correctness? At least in a broader sense?
Even SPJ (one of the designers of the language) admits laziness should retire now (it was a good experiment to develop purely functional ways, but now the disadvantages outweigh the advantages), but most Haskellers will never agree with this because laziness does also have advantages, and they love it. I actually gave up waiting for the Haskell community to abandon pervasive laziness, so just moved to other languages.
Sometimes I get a sense that many Haskellers treat hardware as merely implementation details.
I certainly wouldn't hold my breath for Haskell abandoning pervasive laziness. It's a core principle of the language (Haskell is, after all, "being lazy with class"); if it were removed, the result would be Haskell in name only.
If you don't want pervasive laziness then using a different language is absolutely the correct thing to do. That's not a bad thing; it's just a case of having different tools for different jobs. Haskell may be the best fit for some projects but not others.
This is the ideal. The pinnacle of abstraction that HL languages strive for.
When you (inevitably) need to think about hardware in a extremely complicated system it means you hit a design flaw in the abstraction.
What a lot of haskellers don't realize is that laziness can be one aspect of this flaw.
If you want a ML-derivative without pervasive laziness, there are plenty out there (F#, OCaml, Alice ML and others.) It would make more sense to extend those with appropriate type-system features than try to redesign Haskell from the ground up without pervasive laziness.
Do you have a source for this? I recall him saying something similar but different in spirit.
Scheme is deceptively simple though,.. so simple that you won't recognize it's beauty and uniqueness until you're really deep in. When I started learning scheme I quickly lost interest due to the fact that I felt I wasn't learning anything new.
It wasn't until I discovered sicp and had my mind blown again and again after every lecture did I realize how great lisps are. I recommend checking the course out: http://ocw.mit.edu/courses/electrical-engineering-and-comput...
I would have thought, if laziness-by-default was your main problem with Haskell, a proper alternative would be something like SML or OCaml, not a Lisp/Scheme.
It's also a problem that tends to be exaggerated and is certainly not a reason to abandon laziness as others (not you) in this thread have suggested.
The equivalent of full-on garbage collection would be perfect, of course, and we'll get there eventually for non-strictness and meanwhile the existing system and tooling is improving all the time.
Finally, I think people also overestimate how much these things affect the popularity of a tool. I remember having pervasive, hard-to-track memory leaks in JavaScript just five or six years ago, and look how far it's spread by now!
This is like trying to debug leaks in C with printf. It is just not done. Where's Haskell's valgrind-like tool?
http://book.realworldhaskell.org/read/profiling-and-optimiza...
The question then becomes, why couldn't the author use them to help pinpoint the source of his issues?
When reasoning about Haskell code, due to non-strict evaluation you get a kind of "spooky action at a distance." If your program flow is something like "let result = C(B(A())", where A, B, and C are functions, an expression deep within function A may not be evaluated until you are in the middle of evaluating C -- or possibly never evaluated at all, if the value of that A expression isn't required to compute the return value of C. To reason about the runtime behaviour inside A, you may have to reason about the expressions that consume the return value of A. This can feel like chaos to someone who works with imperative languages, and who is used to nice things like explicit, linear stack traces: in Haskell, evaluation is driven by need, not by imperative instruction, and so the execution flow can look inside-out at times. That's not to say that better tooling isn't possible -- but it is a challenging problem.
I don't know about spooky, it makes more sense if you think in terms of "call by need".
That's a statement which could apply to a lot of programs...
Whatever it's called/how it occurs exactly, the result is the same: excessive memory usage. This is what most people will notice, and the term "memory leak" is usually used to encompass all the ways in which this can happen.
Imagine you're in a loop, where every iteration you add one to a number.
In C, you increment the number, write it back, and everyone is happy. In Haskell, the number is not incremented then and there, but instead the runtime replaces the value with the unevaluated expression (n+1). The expression is only evaluated when its used. Then the value is replaced with the result of the expression.
If you don't evaluate the expression, though, then the next iteration you get ((n+1)+1). And then (((n+1)+1+1). And so on until you run out of memory and the process falls over.
While this is, technically, just another memory leak, they're distinctive enough to have acquired a different name. (To distinguish from, say, cached data building up in a data structure.)
This is one of the biggest gotchas with Haskell programming, unfortunately; because the program is actually correct it's very hard to debug these. (Note that the author has to rely on trial and error a lot, and gives up completely on the last one. And the author's an experienced Haskell programmer!)
I wouldn't consider it a memory leak* at all, the memory is not lost as it is in a leak, space leaks get cleaned up if they are evaluated before you exhaust memory. Memory leaks are not cleaned up until your process dies. I can cause a 1gb space leak 100k times and it will only ever use 1gb of memory. I cannot do that with a memory leak.
* the literal combination of "memory" and "leak" greatly imply the loss of the memory, as is generally what happens when you describe something as leaking; you lose it. additionally, wikipedia[0] seems to agree that the loss of memory is part of the definition.
While the end results are similar, the mechanisms are wildly different, and just because _you_ don't differentiate between them doesn't mean that other people can't, or that it's not a useful distinction.
Space leaks are an implementation detail. The program may be correct, but not perform well on some particular implementation of the language; it may perform well on a different implementation. To fix the problem, we can either alter the implementation (eg. adding supercompilation, adding fusion rules, performing speculative evaluation, etc.) or we can alter the source code. In either case, the input/output behaviour is the same: we get a different implementation of the same program.