Note that there is a elephant in the room that the answers fail to reflect on. The questioner asks:
How can this paradigm be used to build predictable software that works as intended when we have no guarantee when and where an expression will be evaluated?
While it is true that this will not lead to unexpected computations in a pure language, it may lead to excessive memory use. For instance, consider the definition of Data.Map.Lazy.Map:
The Map data type is spine-strict and key strict in Weak Head Normal Form (WHNF). However, it is not strict in its values, and values may be thunks (unevaluated expressions). This may result the memory usage of the values to be much larger than when they are fully evaluated.
There is now also a variant of Data.Map that is value-strict, but only in WHNF, so only the outermost constructors may have been evaluated, and you'd have to evaluate the data further to have thunks replaced by actual values.
Thunk leaks are something that every new Haskell programmer will bump into and is something that makes programs less predictable if you are not yet comfortably reason about strictness and laziness.
Edward Yang wrote a nice explanation of such leaks:
I very much enjoy Haskell, but I think that the benefits of laziness are much overstated. Strict languages are often easier to reason about and laziness can often be emulated when necessary. That said, Haskell has many other attractions ;).
I haven't used Haskell enough to say that this is anything more than speculation, but I would imagine that the converse would be true. Python is an excessively eager-evaluating language[1], and trying to emulate lazy evaluation in Python requires enough back-bending that it probably wastes more time/memory than it saves.
In Haskell, since you have guarantees about monadic purity, I would speculate that an alternative runtime could use a secondary thread to 'walk' the tree and evaluate the thunks whenever resources are being under-utilized (or when memory is tight). Perhaps that would be effective enough to be a net improvement?
trying to emulate lazy evaluation in Python requires enough back-bending that it probably wastes more time/memory than it saves.
Wholesale, yes. But one could e.g. use a generator in Python to emulate a infinite list.
I would speculate that an alternative runtime could use a secondary thread to 'walk' the tree and evaluate the thunks whenever resources are being under-utilized (or when memory is tight).
But the thunks could also evaluate to large or infinite data structures, leading to resource starvation when such a strategy is followed.
IMO if the language uses a lazy evaluation regime, the programmer should decide on where strictness can be applied. It's fairly easy to enforce strict evaluation in Haskell via bang patterns, seq, or deepseq. It's reasoning about laziness that is difficult for newcomers.
> It's reasoning about laziness that is difficult for newcomers.
I'm not so sure - for people who come with a more mathematical background, this is actually easier than thinking in terms of strict evaluation, in which every expression is computed even when it's completely unnecessary. When I'm writing mathematical equations down, I don't stop and think of the computational complexity of every reduction/simplification I make; only of the final result.
Haskell may be a bit difficult for a complete beginner to programming, but for reasons completely unrelated to lazy evaluation, and it makes up for that by being more forgiving in many other ways.
The "current" batch of programmers today seem to come from a more computational/imperative mindset, but I don't think that's any more natural than a mindset which emphasizes expression-based reasoning and lazy evaluation. It's possible that, if Haskell shows its appeal in other areas enough, in a few years people will be talking about how much easier lazy evaluation is than strict evaluation, since you don't have to worry about, eg. accidentally computing an infinite sum only to discard it immediately!
Hakdell is easy to reason about for meaning, but hard to reason about for performance: like math, where no one thinks about performance. The folks over in applied math and engineering worry about things like computing an integral quickly and accurately, while the pure math folks are showing how to split a sphere into two exact copies of itself using an algorithm that requires an uncountably infinite set of actions.
> How can this paradigm be used to build predictable software that works as intended when we have no guarantee when and where an expression will be evaluated?
s/an expression will be evaluated/commands are fired/
reveals the latent imperative mindset.
FP programs are designed by laying out, not unlike how train tracks are laid out, transformations from input to output. Some of these transformations are too complex to get right all at once, so they get broken down into smaller ones which are then composed together. (Obviously, there are other reasons why composition is a win.)
An FP program "works as intended" because the transformations from input to output are correct. In high-end software some, possibly even all of it are rigorously proven correct using mathematical reasoning.
The two main ways of correctness reasoning about transformations is with strict or with non-strict semantics. Time/space usage, i.e. the performance characteristics, is treated as an operational concern. That's when the nitty-gritty of eager and lazy evaluation come into play.
The bottom line is that if all time is spent stressing over evaluation, you're optimizing prematurely.
I feel like I should point out that we have no guarantee when and where an expression will be evaluated is also true in languages like C, to an extent. The compiler is free to re-schedule operations that don't have a side effect, just as in Haskell - it's just that this happens less since so many actions could potentially have side effects in C.
I guess the benefit of lazy by default is that (given Haskell is an experiment in purity) it sets the expectation that things will be side-effect free by default.
Were that not a design criteria it might have been better to make things strict by default and have a "lazy" monad rather than do lazy by default and use monads for all the side effecting stuff.
Total functions embed into partial ones, which is why we have a partiality monad. The opposite arrow doesn't exist, which is why we don't have a totality monad.
Now lazy and eager are properly adjectives that govern beta reduction, that is, the interaction between a function and its argument.
The short of it is that neither an eager nor a lazy monad makes any sense.
(Heh, I'm totally citing this in the monads class I teach as proof of why unpacking monads starting from functors is a win.)
How can this paradigm be used to build predictable software that works as intended when we have no guarantee when and where an expression will be evaluated?
While it is true that this will not lead to unexpected computations in a pure language, it may lead to excessive memory use. For instance, consider the definition of Data.Map.Lazy.Map:
http://www.haskell.org/ghc/docs/latest/html/libraries/contai...
The Map data type is spine-strict and key strict in Weak Head Normal Form (WHNF). However, it is not strict in its values, and values may be thunks (unevaluated expressions). This may result the memory usage of the values to be much larger than when they are fully evaluated.
There is now also a variant of Data.Map that is value-strict, but only in WHNF, so only the outermost constructors may have been evaluated, and you'd have to evaluate the data further to have thunks replaced by actual values.
Thunk leaks are something that every new Haskell programmer will bump into and is something that makes programs less predictable if you are not yet comfortably reason about strictness and laziness.
Edward Yang wrote a nice explanation of such leaks:
http://blog.ezyang.com/2011/05/anatomy-of-a-thunk-leak/
I very much enjoy Haskell, but I think that the benefits of laziness are much overstated. Strict languages are often easier to reason about and laziness can often be emulated when necessary. That said, Haskell has many other attractions ;).