Wait, the first suggested fix for "lazy evaluation consumes too much memory" is "make it eager"? I'm sure I'm missing some nuance here, but that seems backwards.
Lazily evaluating 2+2 requires storing two numbers and an operator for a while, eagerly evaluating requires storing just one number (4, the result).
This can be a real problem, for example if you read potentially large network input eagerly and use it to compute a small result lazily. IMO it's always a mistake and IMNSHO the proper solution is always something more nuanced than just "make it eager" or "make it properly lazy".
> you simply need to put a "!" before the term to evaluate in order to get eagerness
That's wrong. With the bang pattern, evaluation only goes up to WHNF, not the whole way — it's just a syntax sugar around "seq" which has been around since always. If you need to fully evaluate the term, look at the deepseq package: it's implementation is non-trivial.
There's no inherent reason why doing lazy evaluation in an eager-by-default language should be any harder than the reverse. I can imagine a language that is eager by default but has a '#' operator that holds expressions in unevaluated form until terms are requested.
The combination of lazy evaluation and state mutation/side effects can be pretty difficult to reason about. For example, if you have a function that changes a global variable as a part of a lazy computation, once that function could have been called you have no way of knowing if or when that global variable will change in the future. If you have other functions that depend on the value of that variable, their future behavior is now much more challenging to reason about than in a strict language. You can also imagine something akin to a race condition in which there are multiple lazy computations which could eventually set that variable to different values and the actual sequence of state transitions depends entirely on the dependency order of a possibly unrelated piece of code. In practice, this means that in languages that are strict by default, lazy computations are often forced to run in order to reason about the code, rather than because the actual results of the computation are required.
Since pure functions compute the same results under lazy or strict evaluation and require that any data dependencies they have are explicitly provided as inputs, they interact with lazy computations in a much more tractable way. This means that adding a strictness operator to a lazy language is much easier than adding a laziness operator a a strict language.
An alternate approach is what python did with generators where there is a data type for lazy computation, but it lives apart from the rest of the language, so it is mostly used for e.g. stream processing where a default-lazy approach is conceptually straightforward and is less likely to lead to extremely non-trivial control flow. This approach does, however, basically give up on having a laziness operator that will turn a strict computation into a lazy one.
Sadly even though what you say is true, I do not know of any languages that get that right. Even those that do care enough to give you a way to make lazy values, they require you to explicitly wrap them and force them all over the place, making lazy programming effectively so noisy it's unusable. Which is the main reason why I still find myself coming back to haskell (stuff like parser combinators is just so unwieldy without laziness and do notation...)
Counterpoint: C++ expression templates have existed for a long time, and require no explicit work on the user side. They are incredibly unwieldy to write and debug though.
I am gonna be completely honest, I have never quite been able to understand expression templates properly. is it possible to use them to get hassle-free laziness? is there any example of a library that does this (just to look at it and see how it's done)?
Of course, C++ is powerful enough that using specific tools can make the expression template abstraction fall apart (e.g. auto), but fundamentally you can write matrix code very cleanly and get great lazy-evaluated performance.
While you're technically right, it's only in the same sense that any TC language can do anything any other TC language can -- you can theoretically always delay any computation behind a lambda, but the ergonomics have wide-ranging consequences. You also don't get any memoization that way -- without further runtime support.
Yes. Which is my main criticism of Haskell: sure, sometimes laziness is very handy, sometimes it's even indispensable... but most of the time, eager is what you want, with judicious sprinkles of explicitly lazy structures (btw, OCaml has "lazy" keyword exactly for that).
Sure, GHC goes to heroic lengths and turns as much lazy computations as it can into eager ones, but there is always a limit to what it can do on its own, and you inevitably end with the programmer having to slap strict patterns and seqs and deepseqs wherever they can reach.
In OCaml lazy data is fundamentally incompatible with strict data -- you have to explicitly evaluate it, e.g. when passing to a function which takes strict data. This means you end up with two disjoint 'worlds' where you need to write algorithms twice, etc. (In fact you'd have to do that for every possible variant of where exactly the laziness lies.)
The ergonomics become incredibly bad, unfortunately.
(Also see my link to a Reddit thread elsewhere in this thread further ideas on why strict-by-default is not a simple win.)
Yes, it's true, and the split is also painful in Haskell. The split exists in Haskell for a choice few data structures where it truly matters for performance -- it also exists for Set/Map. (And I'd argue it is actually a bad thing from a purely language perspective, but sometimes practicality does win out, even in Haskell.)
I think it might actually have been a mistake to have the lazy variants of these data structures, esp. Text/ByteString. IME it's extremely rare to want laziness for these... and it's usually just when building output strings (where there are better solutions like Builders). Anyway, I digress...
IME the same applies to lazy Map/Set, but I'm less familiar with uses for the lazy versions of these. (IIRC they're only spine-lazy, which is rarely what you want, tho.)
> And in LISP IIRC lazy data are type-compatible with strict data.
LISP is dynamically typed -- it's trivial to do there. Also not the directionality: You can go from lazy to strict (that's just evaluation which the runtime will do for you), but you can NOT go the other way automatically. Recovering laziness from strictness is impossible.
The problem with the split happens when you have mismatching types that your compiler will shout at you for.