The meme is only relevant for memory allocations. In a lazy language you don’t always know what memory the runtime is holding on to as you haven’t consumed the full result of a function.
In practice, you only encounter this problem in very rare situations. I have personally never had to care of bad performance in Haskell in any of my projects.
In some projects I had to deal with high memory consumption problems. I could reason about why with ease: I was deciding very large JSON structures in hundreds of threads. Any other language would have had the same high memory profile.
def h(a) = {
val b = f(a)
g(b)
}
and this is taking too long, I can examine f and g separately; I know that h(a) will take as long to run as f(a) plus g(b) (plus a little bit). In a lazy language I have no clue.Printing out the result of g(f(a)) is 2 times faster than printing just f(a). In a non lazy language f(a) and g(f(a)) would take roughly the same amount of time.
In this example the expensive calculations take much longer than printing the output to the terminal. (no nitpicking please)
The performance can only be at most f + g. It may take less time in a lazy language, how is that difficult to reason about, and why would you care if it runs faster?
Imagine you've got a performance problem because f + g used to take x time but now takes 4x. You start trying to break down the problem, and you see that f takes 2x time and g takes 2x time. You check out the old tag where f + g takes x time and you discover that f takes 2x time and g takes 2x time. Where do you even go from there?
If anything, it can make things run faster, but if something is already slow, you just start by looking at the slowest function. You don’t even have to guess, the Haskell profiler will tell you what function that is, even with laziness involved!
Because the slowdown didn't happen in either function. f still takes the same time it used to. g still takes the same time it used to. But somehow the composition has gotten significantly slower. Both functions may be slow when run in isolation, but it's impossible to tell whether one or both is "supposed to" be slow. Profiling and looking at the slowest function can easily lead you on a wild goose chase here.
Could you formulate an example where the composition would be asymptotically slower only for lazy languages?
As I said before, laziness is actually an optimization strategy. If anything, it can only make the composition faster than it would be in a strict language (although rarely so)
No; we both know no such thing exists. The same code will always be asymptotically faster in a lazy language.
The problem is that lazy languages (or at least, the one lazy language in widespread use) encourage a style of code that would be asymptotically slower if written in a strict language. In a strict language, a function is slow or fast, and a slow function is always a problem - which means performance is easy to reason about. In a lazy language, a slow function is not necessarily a problem, depending on how it's used, and so you can't understand performance without understanding the whole program.
An unreliable optimisation can be worse than no optimisation at all. Worst practices should be hard. In theory an extremely vigilant development team could always write code that would be fast if executed strictly, and therefore guarantee no performance problems, but this would require an enormous manual effort since the language doesn't help you with it at all.
Sure. There's nothing to stop you using lazy values in a strict language if you want to; it's just that you get the option of using strict values as well. Whereas in Haskell you have no way of building strict constructions other than some ad-hoc annotations.
They implement the language server protocol and add ide-like functionality for Haskell to any editor that support the protocol like vscode.
I totally agree that the records system is annoying.
If anything, in a lazy language it may take less time, but never more than the sum of both.
How is it that you cannot reason about the performance of each separate function?
How would you solve an iterator in java producing infinite values?
If anything, the performance of a lazy language would be better than a strict one in the presence of an infinite sequence, as it gives you the opportunity to stop sooner and not enter in an infinite loop.
Example in haskell:
main = do
let a = [1..] -- infinite list
print (a !! 0) -- only prints the first element and exits
Example in python: def f():
a = []
b = 0
for True:
a.append(b++)
return a
def g():
print f()[0] # Takes infinite timeIn practice an infinite loop in Python is often an immediate error (stack overflow) at the point where it's declared. In cases like you showed it's a little fiddlier, but any execution of that code will immediately show that there's an infinite loop (e.g. any unit test of it will time out) and a stack dump taken while the program is "stuck" will show exactly where the infinite loop is (which is therefore where the error is, because an infinite loop is always an error in these languages). Which is still not an ideal situation, mind; I'm very much in favour of an Idris-style language where function termination is actually checked by the type system and there's a type-level distinction between data and codata.
> How would you solve an iterator in java producing infinite values?
I'd avoid ever passing around a bare iterator, ideally with a lint rule.
The point isn't that it's impossible to have lazy values in other languages. It's that it's practical and idiomatic to avoid them, or at least limit them to cases where you absolutely need them.
The effects were only visible in production, as it was the only environment where the error condition happened. It caused thousands and thousands of messages to be queued in vain.
It took me, and another person 3 days to review the code line by line to understand where the problem was.
As you said, any execution of the code would show that there is a problem. Just that un this case no one new where it was executing from.
I think you should drop the argument that performance with a lazy language is difficult to reason in terms of time. It is well studied that asymptotically, strict and non-strict languages are the same.
It is a different story, and a valid argument for Memory consumption, though. It seems to me that you read the “impossible to reason about” meme and formed your own opinion of why that would be so. But this is not the actual reason.
If you write the same code in a strict language or a lazy language, the asymptotic performance will be the same, sure. But that ignores the fact that people don't. Lazy languages use different idioms (there would be little point in laziness as a language feature if people didn't write code that relied on it), which is what results in code whose performance is impossible to reason about.
Not because the language is strict, it makes infinite loops or infinite data structures more obvious to debug or understand.
In Haskell it would have been the same exact infinite loop.
There's a lot I don't like about Go, and plenty of things to be cautious about in message-queue-based systems; laziness is by no means the only thing I worry about. "decrementing a counter" suggests the kind of imperative control flow I would be very cautious about; I favour iterating/recursing over data structures, which is inherently safe against infinite loops (but only in strict languages with immutable datastructures!). I can't remember the last time I decremented a counter that was being used for a loop termination condition in the code itself; I've occasionally written functions like "run this block n times", but the most trivial test of such functions tells you whether they're infinite loops or not. Of course this is only really practical in a language powerful enough to write recursion-schemes style constructions.