3 ms·
Memory and Haskell's function calls being allocated on the heap doesn't seem at first glance to be tied to laziness. You could have a strict language with a gro
by dwohnitmok 6y ago
Memory and Haskell's function calls being allocated on the heap doesn't seem at first glance to be tied to laziness. You could have a strict language with a growable stack that effectively puts function calls on the heap (I mean it's usually not actually the heap per se, but the shrinking and growing of the stack makes it similar from a performance perspective). That's usually not done for ABI compatibility reasons rather than any intrinsic reason.
And moreover I think the majority of programmers can immediately spot when something could lead to a stack overflow. I can't recall the last time I had an issue with a stack overflow that wasn't an infinite looping bug. It's not a difficult problem to begin with in strict languages. This is even more true when I think back to how many times I've written a recursive function by hand rather than use a higher order combinator in functional code. I think less than 1% of the code I write is even recursive to begin with.
Space leaks are a pretty annoying problem in Haskell programs though. There have been quite a few large, widely-used Haskell programs that have battled with them. Heck Shellcheck's author dropped multithreading because he couldn't figure out the issue with space leaks. I can't think of any large program that has ever been forced to a drop a feature because they couldn't solve a stack overflow from overly deep recursion (it is a purely mechanical transformation to take any recursive function's call stack and pass it in instead as an explicit stack on the heap).
And being able to locally reason about performance rather (i.e. only needing to take into account the implementation of a function) than having to take into account nonlocal reasoning (how your function will be called) is a huge win on par with the analogous gain of local vs nonlocal reasoning for correctness in functional programs.
I agree it's a matter of experience and changing your way of thinking, but personally I'm way more confident about what the performance profile of a program I've written in Purescript will look like before extensive testing than what the performance profile of an equivalent Haskell program will look like before extensive testing.
- wyager 6y ago> You could have a strict language with a growable stack This doesn't fix the more general problem of something that is generated by a (co-)recursive function and consumed gradually. Non-strictness lets you "pause" the recursion until you need it. You can have a data structure that's bigger than memory (even infinite) and it's not a problem as long as you don't need all of it at once.
- dwohnitmok 6y agoSure, but you can fix that with any number of streaming libraries in strict languages those times when you do actually need that. And importantly, in line with local reasoning, in a strict language, it will be very apparent when you need it. It won't take you by surprise. On the other hand, Haskell's default list and its associated functions are just enough to let you get by but have enough pitfalls that can you take you off-guard that in the end, for more complex operations, you just end up using a streaming library anyways (e.g. conduit, pipes, streamly, etc.).
- wyager 6y ago> you can fix that with any number of streaming libraries in strict languages This is moving the goalposts a bit. Certainly you can do streaming in a strict language if you're willing to use libraries, but we're talking about language primitives. > you just end up using a streaming library anyways You generally only do this if you're interleaving effects with your streaming. I use guarded corecursion all the time, e.g. when I write an expression like index = zip [1..] Any strict language that supports something like `[1..]` has a lot more crap to deal with, and usually has to implement something like overloading list expressions to create iterators, which have their own problems.
- dwohnitmok 6y ago> This is moving the goalposts a bit. I don't think so. My original comment was focusing on stack overflow errors, but my wider thesis is this: By and large, strict languages trade off some level of syntactic convenience for easier, local reasoning about performance. And I am willing to accept that hit. So for example, yeah sure maybe you have to do a mechanical transformation of a recursive function to either use a trampoline or an explicit stack or rewrite with an iterative loop, but that's an acceptable trade-off for me. Stated another way: The cases where a lazy language can cause weird performance issues are harder to debug than the cases where a strict language can cause weird performance issues. Indeed the fact that a library is all that is needed to patch up a lack of language primitives makes me more willing to accept the trade off of syntactic convenience. index = zip [1..] vs index = Stream.zip (Stream.from 1) or even conversions to and from lists if you really don't want streams elsewhere such as index xs = Stream.toList . Stream.zip startIndex $ streamedXs where startIndex = Stream.from 1 streamedXs = Stream.fromList xs is a sufficiently small price for me to pay to gain back local performance reasoning (I mean in this case I'd just write index with a fold to begin with in a strict language). > interleaving effects with your streaming But the issue that effects are supposed to be values in and of themselves (i.e. if I really take effects as a value seriously, why must my machinery change when I have a list of IO actions vs a list of integers?). This is really a symptom of the deeper issue is that there are a lot of things that should be strict for other reasons that can interfere with purely lazy lists or other structures because they basically represent this sort of weird interleaving of strict and lazy needs (this is why e.g. foldl' fails without strict data fields).
- jose_zap 6y ago> I can't think of any large program that has ever been forced to a drop a feature I have to anecdotes of having to drop features in a large python program due to memory leaks. The first one was when someone introduced an endpoint that consumed data from a gRPC service. After weeks of effort trying to find where the source of the leak was, we decided to drop the change as we could not find the cause in a reasonable time. The second case was when introducing jaeger for distributed tracing. Similarly to the previous story, it was cheaper for us to drop the feature and use something else than to figure out where the memory leak was. I believe that it is a matter of balancing effort vs, benefit. I would not be surprised if someone working on their free time on an open source project decides to drop a feature because they cannot find the time to make it work properly.
- dwohnitmok 6y agoI can totally believe that and have fought similar bugs (basically unexpected references that keep memory around way longer than necessary), but memory leaks of that form affect both strict and lazy languages, i.e. it's an independent problem. Lazy languages can have that same problem in addition to space leaks.