For a language that prioritizes "safety" above all things, there is an awful lot of flying blind and dangerously in Haskell. It's so, so easy to write Haskell code that's safe until you change something distant in the system, which changes when things get lazily-evaluated, and now you have a very serious resource leak. And because of the IO restrictions, you aren't likely to put logging in your code - and if you do, the logs will themselves change the lazy evaluation behavior of your code. I've seen Haskell programs that stop crashing when you pass in --debug !
If the Haskell environment was more like a virtual machine - like in Java - where you could connect into a side-channel and see what types of data were persisting in memory as the program ran - you'd at least have a chance of debugging this sort of thing. But instead it compiles to machine binaries.
There doesn't seem to be any interest in the Haskell community in making tools to deal with this sort of thing - they say "you should learn not to make resource-leaking code". Which is the same thing the Lisp hackers say - "just learn not to make type errors".
It is true that Haskell is about denotational safety (correctness) not operational safety (performance limits).
You do not need IO limitations for logging. You can use Debug.Trace
Haskell does have a heap analyzer. Like C, you can choose to compiler an instrumented binary with debug symbols.
There are a huge suite of tools coming around this year. Debugging and analysis has been a huge theme recently. Simon Marlow's recent book gives a taste, as does the latest Communities And Activities Report.
I think this is more of an implementation issue than a language issue. A haskell compiler could choose to reevaluate everything every time it's demanded, and it would run in very little space. GHC uses thunks to represent computations which haven't been run yet, which are replaced once any threads evaluates them. This can lead to space leaks, but I don't believe there's any requirement that this be how the results are evaluated. GHC chooses this evaluation strtategy because it usually results in faster code, but obviously can lead to problems if you don't know what you're doing. Like all languages, it takes experience to avoid whatever deficiency exists in your language of choice; it won't be obvious to a beginner why the naive implementation of fibonacci works for some values but overflows the stack on others, but they will eventually learn.
Haskell the language is concerned with proving small theorems about the program as specified by the source code. Actually running it is outside the scope of language specification; it's an implementation detail. GHC (the defacto compiler for Haskell) makes a lot of resource tradeoffs to try and achieve better performance.
If you take a little care to e.g. make data types' fields strict, this tends to be a non-problem. It is harder to debug the code since it is lazily evaluated (and it's not imperative!), but recent versions of GHC provide pretty powerful profiling, stack traces, etc.
I'm not sure that I would agree that you're flying "blind or dangerously" either way--at least not when it comes to building up large unevaluated thunks. I would agree somewhat if you were referring to code using unsafePerformIO, which makes it the programmer's responsibility not to break referential transparency. While that doesn't tend to be an issue either, Safe Haskell does mostly solve it.
I disagree. I've never seen a program fail because of an unsafePerformIO call, but I've never seen a Haskell project that didn't suffer from at least one mysterious, hard-to-solve unevaluated-thunks-filling-up-RAM bug.
I've never seen a Java application that hasn't succumbed to an unexpected NullPointerException at least once over its entire development cycle. That doesn't mean that the language isn't a reasonable choice. It's just a common problem that you have to accept on that platform.
Space leaks in Haskell are similar. You're going to run into them every so often, and you'd really rather not, but they're easy enough to deal with.
I'd take a NullPointerException over a memory (edit: space) leak any day of the week; the former is instantly resolved, the latter, ?.
Anyway, in Java 8 they've started taking steps to address the null problem with the new Optional type, and, FWIW, in Scala nulls are a more or less a non-issue when you use the FP side of the language.
I think there may be some confusion about what a space leak in Haskell is.
When you apply a function f to a, that is not actually evaluated. Rather, a "thunk" is created that will evaluate `f a` only when that value is actually needed.
If, in your program, you never need anything, or don't "force" your function calls and data structures ("pretend" to need something) in intermediary stages, then the thunks may take up a non-trivial amount of memory. This is not a memory leak in the traditional sense, just temporarily increased memory usage.
It is very easy to (pre-emptively) handle most space leaks in Haskell, but you do need to know how they arise.
There are two very simply rules you can follow that take care of the vast majority of space leaks:
1. Make data fields strict unless you actually want them to be lazy, i.e. instead of:
data Foo = Foo
{ bar :: String
, baz :: Int
}
write
data Foo = Foo
{ bar :: !String
, baz :: !Int
}
2. When you write recursive functions that depend on values which are not forced (e.g. pattern matched against) in each function call, use either `seq`/$! or bangpatterns to make sure the value is evaluated (to HNF) rather than building up excessive thunks. For example, instead of:
acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock (connNum+1)
write either
acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock !connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock (connNum+1)
or
acceptLoop :: Socket -> Int -> IO ()
acceptLoop sock connNum = do
econn <- accept sock
_ <- case econn of
Left err -> printf "Error accepting connection %d: %s" connNum err
Right conn -> forkIO $ runConn conn
acceptLoop sock $! connNum+1
to make sure that connNum is always just a single value rather than a series of unevaluated thunks. That way you won't get a space leak if you rarely have problems accepting new connections.
Now, that begs the question, why lazy by default and not opt-in lazy?
From the outside looking in it seems that deep expertise is required in order to launch a Haskell production app with any degree of confidence (i.e. to quickly dig yourself out of runtime issues like space leaks where the means to avoid them may be known, but the means to resolve them when they occur, non-trivial).
> Now, that begs the question, why lazy by default and not opt-in lazy?
Very good question. Actually, I think most haskellers agree that laziness complicates things more often than not, and if we could start over we wouldn't make Haskell lazy by default. (Although that's not to say there won't be even simpler ways to "strictify" things in the future. Also, many libraries already provide functions that are strict in their arguments by default.)
However, there is also agreement that Haskell's laziness is the reason the language got purity right: there was simply no other way, since laziness meant evaluation order was unclear.
> From the outside looking in it seems that deep expertise is required in order to launch a Haskell production app with any degree of confidence (i.e. to quickly dig yourself out of runtime issues like space leaks where the means to avoid them may be known, but the means to resolve them when they occur, non-trivial).
As someone who writes Haskell for a living, I really just follow a few rules like this without thinking too much about laziness, and I tend to not have any problems. I have had maybe one nasty space leak in the past five years.
Yes, sometimes they do come up, but it takes ~5-10 minutes to pinpoint the problem spot with the heap profiler. It is not nearly as messy as using Valgrind to find actual memory leaks.
> I think most haskellers agree that laziness complicates things more often than not, and if we could start over we wouldn't make Haskell lazy by default.
Following Haskell's evolution somewhat from the outside, this is surprising. (And also somewhat disappointing, as laziness always seemed an important part of Haskell's elegance.) Is laziness now considered something of a failed experiment?
> laziness always seemed an important part of Haskell's elegance
It's part of it, but to a lesser extent than you would expect. The much more important part of Haskell is its no-corners-cut separation of effects, which happened chiefly because, without it, laziness meant that there was no way to know when fireTheMissiles() would actually happen.
> Is laziness now considered something of a failed experiment?
To some extent, yes. No one is saying laziness doesn't make the implementation of some algorithms and data structures extremely elegant, just that, most of the time, you don't actually gain much by leaving your function calls and data types lazy.
I enjoy being able to make infinite/self-referencing data structures, or leaving fields lazy and "performing" all of the function calls in the initialization of the "struct", but have only the functions producing results that will actually be needed matter performance-wise. However, if you don't use any strictness annotation, that benefit doesn't outweigh the problems that can be caused by space leaks.
If you do use strictness annotation, I don't think it matters too much whether the language is lazy or strict, you just have to write strictness annotation instead of laziness annotation.
I think this might be overstated. It is certainly an opinion with some mindshare; I would hesitate to guess whether it's in the majority or minority, and it's definitely not universal.
Because the downsides of strictness are great as well. Laziness lets you "decomplect" production from consumption, with a lot of safety, most of the time. I think few who've spent most of their time working in strict-by-default languages recognizes the pain of it. Those who do write opt-in lazy language properties like generators.
So the question becomes whether strict-by-default or lazy-by-default is better given that both kinds of evaluation have their place. I personally have come to believe that lazy-by-default is nicer since there are fewer places where I really demand strictness... but it also basically forces your language to be pure.
Without laziness the Haskell community would likely not have maintained functional purity for very long. Looking at the history of all other programming languages it's difficult to find one that hasn't succumbed to the temptation of impurity at some point or another. Without this enforced purity, there would not have been the same pressure to develop technologies such as Applicatives and Monads.
The other advantage of laziness is that it is a big aid to composition, modularity and concision. It allows you to perform common sub-expression elimination which can be a major boon for code readability. See this article for some examples:
Right, to fully evaluate something you'd use Deepseq. With data structures like Data.HashMap.Strict, Data.Text, etc., the HNF gained from strict data fields tends to be sufficient to avoid most issues caused by space leaks -- but even for something like !String, i.e. ![Char], you'd have to try pretty hard to blow up the stack. (Evaluating the first cell is also more useful than it might seem at first glance.)
I find myself using deepseq the most when I want to be sure a data structure (and any exceptions any pending operations might throw) has been fully evaluated before passing it off to another thread, not to prevent space leaks.
I think it was because - in the words of The Dude, "Yeah, well, you know, that's just, like, your opinion, man."
> I'd take a NullPointerException over a memory (edit: space) leak any day of the week.
That's your preference and I can understand that preference from your point of view because in order to debug a space leak in a Haskell program you would first have to learn Haskell.
I'm not the one who downvoted, but I also didn't think it contributed much =(
Why? NPEs are easily solved, even for beginners. Stack trace says blah blah blah occurred at line X in class Y. Easy fix, totally low hanging fruit for beginners and experts alike.
The space leak issue, OTH, may be easily avoided (if one is well versed in Haskell best practices), but resolving them when they occur is something else entirely. Seriously, you have to break out a memory profiler in order to find out where the issue _may be_, not exactly where it is on line X of class Y.
So, yes, I never get them (in Scala), but I'll stand by taking an NPE over a space leak any day of the week.
thirsteh summed up my feelings in his response to your comment but I'd also like to add that a preference is an opinion and not a fact. You may prefer apples over oranges. It is not a fact that apples are better than oranges.
For instance, what if your null pointer in language x causes a silent fail? Now your apples are beginning to look like oranges.
The choice between NullPointerExceptions or occasional space leaks isn't really apples-to-apples. You could similarly ask if you'd rather have solely impure functions and no isolation of effects, or occasional space leaks. Clearly (probably?) the answer would be the latter.
If you're going to have errors anyway (and you are), then it's better to have errors that fail fast and early and in a clearly identifiable way. NullPointerExceptions are easy.
And that's why the 'war' has just moved to the JVM: Closure is a Lisp, Scala steals a whole lot of things for Haskell, trying to make it actually practical.
I think Haskell's ultimate role is a bit like Ruby's: It can't really win, but it's destined to be influential. That's a much harder road for Lisp, as its greatest strengths are also its greatest flaws.
> Scala steals a whole lot of things for Haskell, trying to make it actually practical.
LINQ is "practical Haskell," so is most of Rust (if "practical" means approachable to imperative programmers.)
Besides being on the JVM (which is a big plus), Scala hardly makes anything more practical than it is in Haskell. In fact, Scala programmers tend to migrate to Haskell (and ML languages) rather than the other way around.
If by LINQ you mean the map/filter/fold crew, I think this may be selling Haskell a little short. You could say the same about python. At least F#'s computation expressions give you the full power to create your own monads with it's "computation expressions", and in LINQ things are more statically locked down.
I guess F# isn't practical if you're building a WPF application (I agree the tooling is lacking). Otherwise it seems like it can do everything C# does (including mutation, classes, interfaces, properties), only with a slightly unfamiliar (at first) syntax.
Doesn't scala suffer from the JVM constraints, like no tail call recursion, everything is an object, such that the compile times become enormous to work around them?
Haskell is indeed influencing, but it is not remaining stagnant.
> Doesn't scala suffer from the JVM constraints, like no tail call recursion, everything is an object, such that the compile times become enormous to work around them?
Yes. Yes it does. Scala is a bodge that is destined to be replaced (or at least rewritten), and I say that as a huge fan of the language. I would happily bet on Haskell outlasting Scala in the long run.
(But I use Scala today, because in the long run we're all dead. Scala inherits a lot of useful production infrastructure from the JVM, and on recent progress it looks like Scala can get faster compiles quicker than Haskell can get better infrastructure. Which means that today, in many environments, Scala is the better choice)
The way I understand it, is that Scala does support tail-call recursion by way of compiling to a loop. Compilation was definitely a pain when I experimented with Sit.
Scala supports simple recursive tail-call optimization, but is less elegant in handling mutual recursion (due to JVM constraints).
Clojure is probably the one you're thinking of that doesn't support tail-call elimination at all. Rich Hickey thought that if you couldn't do it cleanly in all cases (like mutual recursion), you may as well just come up with something else. So instead of optimizing recursive calls, Clojure has the recur function.
I concur when it comes to lazy-evaluation. I once lost 10% of my grade in a project because I removed a debugging print statement, which in turn made a data-structure lazily evaluated, which in turn meant that a significant portion of my type-checking was never evaluated.
What? This means that you must have been using exceptions to communicate type-checking information. That's just a terrible way to write Haskell. Exceptions only have nice semantics when they're e.g. thrown in the IO monad.
-- No, no, no, no!
checkValid :: Int -> ()
checkValid x = if x >= 5
then error "Invalid number!"
else ()
-- This works fine.
checkValid :: Int -> Either String ()
checkValid x = if x >= 5
then fail "Invalid number!"
else return ()
This may sound a bit harsh, but I think you deserved to lose that 10% if you had such a fundamental misunderstanding of "lazy evaluation".
Astute observation; I agree that I should not have used runtime errors. I quickly converted the code to a Monad based solution. That being said, I was in the middle of trying to understand both Haskell and my task at hand, so I was quite thankful to have it working at all. With no one that understood Haskell enough to explain Monads to me, I had to slowly come to the understanding of them on my own.
It's really not a bug in Haskell that you ran out of time to learn what you needed before an arbitrary unrelated deadline. I hope you didn't let a silly grade stop you from continuing to learn.
I didn't. I just completed the first phase in which we produce assembly code and will be finishing the project up this month. I hope to put the code up on Github. Perhaps someone else will find it curious and/or learn from my mistakes.
This project has been the most learning-filled project of my college career. I wouldn't trade that for any easy A. :)
> If this opinion is a reasonable example of Haskell programmers' attitudes, I would expect some significant portion of programmers would want to stay the heck away from it.
The opinion that people should get less of a grade if they don't know what they're doing is unreasonable? I'm not a compiler writer, but using exceptions for regular control flow in a type checker sounds very iffy to me, Haskell or not. Why would exceptions even show up in a type checker, for that matter?
Telling a kid in college (I was a sophomore when I was writing Haskell) that he deserves to lose 10% over something like that is crazy. The obvious to us, many years into programming professionally, is not always obvious to newer folks.
The ideal purpose of grades is teaching. It's a way of informing you of the parts of the curriculum that you do not yet fully understand. This sounds like an entirely appropriate situation to lose grade points on.
Unfortunately, grades now also serve as some poor measure of intelligence or competency in many people's minds.
Marking work is a way of teaching, grades are used as a measure of understanding. I had a friend in college that, at the start of a semester, was typically a mediocre to borderline failing student if you looked at the marks on his assignments. By the end of the semester his understanding would be on par with mine (typically As in CS/math courses with the occasional B). However, his grade reflected his poor early start with the material and he'd end up with Cs. Now, that's good enough to pass to the next class, but it still helped to screw him over when he started applying to jobs after college.
A grade is also how they decide if you get your undergraduate degree. It would be a strange shame if that decision had nothing to do with your intelligence or competency as you imply.
Grades are for showing (to teacher and pupil) which students can do the activity in question better than others. There's a lot of BS in modern education that would have you believe otherwise.
> they say "you should learn not to make resource-leaking code". Which is the same thing the Lisp hackers say - "just learn not to make type errors".
Errors are unavoidable, no matter what language you use. The important thing is that you can catch them in testing. I don't value static typing very highly because type errors show up quickly. Pass the wrong type, and execution fails. Ideally this happens in your unit tests. During development. Because you're doing TDD.
Resource leaks, on the other hand, are usually very hard to catch, and often don't show up until production.
> they say "you should learn not to make resource-leaking code". Which is the same thing the Lisp hackers say - "just learn not to make type errors".
<sarcasm> We should just stop writing the bugs. If, instead, we focused in writing code that runs correctly, everything would be much better. </sarcasm>
If the Haskell environment was more like a virtual machine - like in Java - where you could connect into a side-channel and see what types of data were persisting in memory as the program ran - you'd at least have a chance of debugging this sort of thing. But instead it compiles to machine binaries.
There doesn't seem to be any interest in the Haskell community in making tools to deal with this sort of thing - they say "you should learn not to make resource-leaking code". Which is the same thing the Lisp hackers say - "just learn not to make type errors".