What are Haskell's strictness points?
Asked Answered
N

8

92

We all know (or should know) that Haskell is lazy by default. Nothing is evaluated until it must be evaluated. So when must something be evaluated? There are points where Haskell must be strict. I call these "strictness points", although this particular term isn't as widespread as I had thought. According to me:

Reduction (or evaluation) in Haskell only occurs at strictness points.

So the question is: what, precisely, are Haskell's strictness points? My intuition says that main, seq / bang patterns, pattern matching, and any IO action performed via main are the primary strictness points, but I don't really know why I know that.

(Also, if they're not called "strictness points", what are they called?)

I imagine a good answer will include some discussion about WHNF and so on. I also imagine it might touch on lambda calculus.


Edit: additional thoughts about this question.

As I've reflected on this question, I think it would be clearer to add something to the definition of a strictness point. Strictness points can have varying contexts and varying depth (or strictness). Falling back to my definition that "reduction in Haskell only occurs at strictness points", let us add to that definition this clause: "a strictness point is only triggered when its surrounding context is evaluated or reduced."

So, let me try to get you started on the kind of answer I want. main is a strictness point. It is specially designated as the primary strictness point of its context: the program. When the program (main's context) is evaluated, the strictness point of main is activated. Main's depth is maximal: it must be fully evaluated. Main is usually composed of IO actions, which are also strictness points, whose context is main.

Now you try: discuss seq and pattern matching in these terms. Explain the nuances of function application: how is it strict? How is it not? What about deepseq? let and case statements? unsafePerformIO? Debug.Trace? Top-level definitions? Strict data types? Bang patterns? Etc. How many of these items can be described in terms of just seq or pattern matching?

Nomi answered 20/9, 2011 at 19:34 Comment(5)
Your intuitive list is probably not very orthogonal. I suspect that seq and pattern matching are sufficient, with the rest defined in terms of those. I think pattern matching assures the spine-strictness of IO actions, for example.Alloplasm
Primitives, such as + on the built-in numeric types also force strictness, and I assume the same applies to pure FFI calls.Pleasance
There seem to be two concepts being confused here. Pattern matching and seq and bang patterns are ways that an expression can become strict in its sub-expressions -- that is, if the top expression is evaluated, so is the subexpression. On the other hand, main performing IO actions is how evaluation starts. These are different things, and it's a sort of type error to include them in the same list.Forby
@ChrisSmith I'm not trying to confuse those two different cases; if anything I am asking for further clarification on how they interact. Strictness happens somehow, and both cases are important, though differing, parts of strictness "happening". (and @ monadic: ಠ_ಠ)Nomi
If you want/need room to discuss aspects of this question, without attempting a full answer, allow me to suggest utilizing the comments on my /r/haskell post for this questionNomi
W
48

A good place to start is by understanding this paper: A Natural Semantics for Lazy Evalution (Launchbury). That will tell you when expressions are evaluated for a small language similar to GHC's Core. Then the remaining question is how to map full Haskell to Core, and most of that translation is given by the Haskell report itself. In GHC we call this process "desugaring", because it removes syntactic sugar.

Well, that's not the whole story, because GHC includes a whole raft of optimisations between desugaring and code generation, and many of these transformations will rearrange the Core so that things get evaluated at different times (strictness analysis in particular will cause things to be evaluated earlier). So to really understand how your program will be evaluated, you need to look at the Core produced by GHC.

Perhaps this answer seems a bit abstract to you (I didn't specifically mention bang patterns or seq), but you asked for something precise, and this is about the best we can do.

Weepy answered 20/9, 2011 at 22:49 Comment(3)
I've always found it amusing that in what GHC calls "desugaring", the syntactic sugar being removed includes the actual syntax of the Haskell language itself... implying, it might seem, that GHC is in truth an optimizing compiler for the GHC Core language, which incidentally also happens to include a very elaborate front-end for translating Haskell into Core. :]Alloplasm
The type systems don't mach up precisely though... particularly but not only with regards to translating typeclasses into explicit dictionaries, as I recall. And all the latest TF/GADT stuff, as I understand it, has made that gap wider still.Joung
GCC doesn't optimize C either: gcc.gnu.org/onlinedocs/gccint/Passes.html#PassesGammon
S
20

I would probably recast this question as, Under what circumstances will Haskell evaluate an expression? (Perhaps tack on a "to weak head normal form.")

To a first approximation, we can specify this as follows:

  • Executing IO actions will evaluate any expressions that they “need.” (So you need to know if the IO action is executed, e.g. it's name is main, or it is called from main AND you need to know what the action needs.)
  • An expression that is being evaluated (hey, that's a recursive definition!) will evaluate any expressions it needs.

From your intuitive list, main and IO actions fall into the first category, and seq and pattern matching fall into the second category. But I think that the first category is more in line with your idea of "strictness point", because that is in fact how we cause evaluation in Haskell to become observable effects for users.

Giving all of the details specifically is a large task, since Haskell is a large language. It's also quite subtle, because Concurrent Haskell may evaluate things speculatively, even though we end up not using the result in the end: this is a third breed of things that cause evaluation. The second category is quite well studied: you want to look at the strictness of the functions involved. The first category too can be thought to be a sort of "strictness", though this is a little dodgy because evaluate x and seq x $ return () are actually different things! You can treat it properly if you give some sort of semantics to the IO monad (explicitly passing a RealWorld# token works for simple cases), but I don't know if there's a name for this sort of stratified strictness analysis in general.

Shannanshannen answered 20/9, 2011 at 21:24 Comment(0)
A
18

C has the concept of sequence points, which are guarantees for particular operations that one operand will be evaluated before the other. I think that's the closest existing concept, but the essentially equivalent term strictness point (or possibly force point) is more in line with Haskell thinking.

In practice Haskell is not a purely lazy language: for instance pattern matching is usually strict (So trying a pattern match forces evaluation to happen at least far enough to accept or reject the match.

Programmers can also use the seq primitive to force an expression to evaluate regardless of whether the result will ever be used.

$! is defined in terms of seq.

Lazy vs. non-strict.

So your thinking about !/$! and seq is essentially right, but pattern matching is subject to subtler rules. You can always use ~ to force lazy pattern matching, of course. An interesting point from that same article:

The strictness analyzer also looks for cases where sub-expressions are always required by the outer expression, and converts those into eager evaluation. It can do this because the semantics (in terms of "bottom") don't change.

Let's continue down the rabbit hole and look at the docs for optimisations performed by GHC:

Strictness analysis is a process by which GHC attempts to determine, at compile-time, which data definitely will 'always be needed'. GHC can then build code to just calculate such data, rather than the normal (higher overhead) process for storing up the calculation and executing it later.

GHC Optimisations: Strictness Analysis.

In other words, strict code may be generated anywhere as an optimisation, because creating thunks is unnecessarily expensive when the data will always be needed (and/or may only be used once).

…no more evaluation can be performed on the value; it is said to be in normal form. If we are at any of the intermediate steps so that we've performed at least some evaluation on a value, it is in weak head normal form (WHNF). (There is also a 'head normal form', but it's not used in Haskell.) Fully evaluating something in WHNF reduces it to something in normal form…

Wikibooks Haskell: Laziness

(A term is in head normal form if there is no beta-redex in head position1. A redex is a head redex if it is preceded only by lambda abstractors of non-redexes 2.) So when you start to force a thunk, you're working in WHNF; when there are no more thunks left to force, you're in normal form. Another interesting point:

…if at some point we needed to, say, print z out to the user, we'd need to fully evaluate it…

Which naturally implies that, indeed, any IO action performed from main does force evaluation, which should be obvious considering that Haskell programs do, in fact, do things. Anything that needs to go through the sequence defined in main must be in normal form and is therefore subject to strict evaluation.

C. A. McCann got it right in the comments, though: the only thing that's special about main is that main is defined as special; pattern matching on the constructor is sufficient to ensure the sequence imposed by the IO monad. In that respect only seq and pattern-matching are fundamental.

Aureole answered 20/9, 2011 at 21:41 Comment(1)
Actually the quote "if at some point we needed to, say, print z out to the user, we'd need to fully evaluate it" is not entirely correct. It is as strict as the Show instance for the value being printed.Aperture
R
10

Haskell is AFAIK not a pure lazy language, but rather a non-strict language. This means that it does not necessarily evaluate terms at the last possible moment.

A good source for haskell's model of "lazyness" can be found here: http://en.wikibooks.org/wiki/Haskell/Laziness

Basically, it is important to understand the difference between a thunk and the weak header normal form WHNF.

My understanding is that haskell pulls computations through backwards as compared to imperative languages. What this means is that in the absence of "seq" and bang patterns, it will ultimately be some kind of side effect that forces the evaluation of a thunk, which may cause prior evaluations in turn (true lazyness).

As this would lead to a horrible space leak, the compiler then figures out how and when to evaluate thunks ahead of time to save space. The programmer can then support this process by giving strictness annotations (en.wikibooks.org/wiki/Haskell/Strictness , www.haskell.org/haskellwiki/Performance/Strictness) to further reduce space usage in form of nested thunks.

I am not an expert in the operational semantics of haskell, so I will just leave the link as a resource.

Some more resources:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation

Relive answered 20/9, 2011 at 21:13 Comment(0)
W
6

Lazy doesn't mean do nothing. Whenever your program pattern matches a case expression, it evaluates something -- just enough anyway. Otherwise it can't figure out which RHS to use. Don't see any case expressions in your code? Don't worry, the compiler is translating your code to a stripped down form of Haskell where they are hard to avoid using.

For a beginner, a basic rule of thumb is let is lazy, case is less lazy.

Warrigal answered 20/9, 2011 at 21:39 Comment(8)
Note that while case always forces evaluation in GHC Core, it does not do this in regular Haskell. For example, try case undefined of _ -> 42.Pleasance
case in GHC Core evaluates its argument to WHNF, while case in Haskell evaluates its argument as much as needed to select the appropriate branch. In hammar's example, that's not at all, but in case 1:undefined of x:y:z -> 42, it evaluates deeper than WHNF.Tufa
And also case something of (y,x) -> (x,y) needs not evaluate something at all. This is true for all product types.Auberon
@Auberon - that's incorrect. something would need to be evaluated to WHNF to reach the tuple constructor.Thrifty
John - Why? We know that it must be a tuple, so where's the point of evaluating it? It suffices if x and y are bound to code that evaluate the tuple and extract the appropriate slot, should they themselves ever be needed.Auberon
BTW - actually the interpreter seems to evaluate it nevertheless. But in the strong sense it would not need it.Auberon
Just checked case undef­ined of ~(x,y­) -> fst (42, (y,x)­) is 42. Because this can be evaluated, it is not quite clear why without the ~ it is stricter than needed.Auberon
I believe this is part of Haskell's semantics. Without the ~, pattern matches will evaluate data, even if strictly speaking it isn't necessary. With ~, evaluation is deferred.Thrifty
J
4

This is not a full answer aiming for karma, but just a piece of the puzzle -- to the extent that this is about semantics, bear in mind that there are multiple evaluation strategies that provide the same semantics. One good example here -- and the project also speaks to how we typically think of Haskell semantics -- was the Eager Haskell project, which radically altered evaluation strategies while maintaining the same semantics: http://csg.csail.mit.edu/pubs/haskell.html

Joung answered 23/9, 2011 at 21:18 Comment(0)
B
2

The Glasgow Haskell compiler translates your code into a Lambda-calculus-like language called core. In this language, something is going to be evaluated, whenever you pattern match it by a case-statement. Thus if a function is called, the outermost constructor and only it (if there are no forced fields) is going to be evaluated. Anything else is canned in a thunk. (Thunks are introduced by let bindings).

Of course this is not exactly what happens in the real language. The compiler convert Haskell into Core in a very sophisticated way, making as many things as possibly lazy and anything that is always needed lazy. Additionally, there are unboxed values and tuples that are always strict.

If you try to evaluate a function by hand, you can basically think:

  • Try to evaluate the outermost constructor of the return.
  • If anything else is needed to get the result (but only if it's really needed) is also going to be evaluated. The order doesn't matters.
  • In case of IO you have to evaluate the results of all statements from the first to the last in that. This is a bit more complicated, since the IO monad does some tricks to force evaluation in a specific order.
Boggess answered 26/9, 2011 at 13:40 Comment(0)
F
0

We all know (or should know) that Haskell is lazy by default. Nothing is evaluated until it must be evaluated.

No.

Haskell is not a lazy language

Haskell is a language in which evaluation order doesn't matter because there are no side effects.

It's not quite true that evaluation order doesn't matter, because the language allows for infinite loops. If you aren't careful, it's possible to get stuck in a cul-de-sac where you evaluate a subexpression forever when a different evaluation order would have led to termination in finite time. So it's more accurate to say:

  • Haskell implementations must evaluate the program in a way that terminates if there is any evaluation order that terminates. Only if every possible evaluation order fails to terminate can the implementation fail to terminate.

This still leaves implementations with a huge freedom in how they evaluate the program.

A Haskell program is a single expression, namely let {all top-level bindings} in Main.main. Evaluation can be understood as a sequence of reduction (small-)steps which change the expression (which represents the current state of the executing program).

You can divide reduction steps into two categories: those that are provably necessary (provably will be part of any terminating sequence of reductions), and those that aren't. You can divide the provably necessary reductions somewhat vaguely into two subcategories: those that are "obviously" necessary, and those that require some nontrivial analysis to prove them necessary.

Performing only obviously necessary reductions is what's called "lazy evaluation". I don't know whether a purely lazy evaluating implementation of Haskell has ever existed. Hugs may have been one. GHC definitely isn't.

GHC performs reduction steps at compile time that aren't provably necessary; for example, it will replace 1+2::Int with 3::Int even if it can't prove that the result will be used.

GHC may also perform not-provably-necessary reductions at run time in some circumstances. For example, when generating code to evaluate f (x+y), if x and y are of type Int and their values will be known at run time, but f can't be proven to use its argument, there is no reason not to compute x+y before calling f. It uses less heap space and less code space and is probably faster even if the argument isn't used. However, I don't know whether GHC actually takes these sorts of optimization opportunities.

GHC definitely performs evaluation steps at run time that are proven necessary only by fairly complex cross-module analyses. This is extremely common and may represent the bulk of the evaluation of realistic programs. Lazy evaluation is a last-resort fallback evaluation strategy; it isn't what happens as a rule.

There was an "optimistic evaluation" branch of GHC that did much more speculative evaluation at run time. It was abandoned because of its complexity and the ongoing maintenance burden, not because it didn't perform well. If Haskell was as popular as Python or C++ then I'm sure there would be implementations with much more sophisticated runtime evaluation strategies, maintained by deep-pocketed corporations. Non-lazy evaluation isn't a change to the language, it's just an engineering challenge.

Reduction is driven by top-level I/O, and nothing else

You can model interaction with the outside world by means of special side-effectful reduction rules like: "If the current program is of the form getChar >>= <expr>, then get a character from standard input and reduce the program to <expr> applied to the character you got."

The entire goal of the run time system is to evaluate the program until it has one of these side-effecting forms, then do the side effect, then repeat until the program has some form that implies termination, such as return ().

There are no other rules about what is reduced when. There are only rules about what can reduce to what.

For example, the only rules for if expressions are that if True then <expr1> else <expr2> can be reduced to <expr1>, if False then <expr1> else <expr2> can be reduced to <expr2>, and if <exc> then <expr1> else <expr2>, where <exc> is an exceptional value, can be reduced to an exceptional value.

If the expression representing the current state of your program is an if expression, you have no choice but to perform reductions on the condition until it's True or False or <exc>, because that's the only way you'll ever get rid of the if expression and have any hope of reaching a state that matches one of the I/O rules. But the language specification doesn't tell you to do that in so many words.

These sorts of implicit ordering constraints are the only way that evaluation can be "forced" to happen. This is a frequent source of confusion for beginners. For example, people sometimes try to make foldl more strict by writing foldl (\x y -> x `seq` x+y) instead of foldl (+). This doesn't work, and nothing like it can ever work, because no expression can make itself evaluate. The evaluation can only "come from above". seq is not special in any way in this regard.

Reduction happens everywhere

Reduction (or evaluation) in Haskell only occurs at strictness points. [...] My intuition says that main, seq / bang patterns, pattern matching, and any IO action performed via main are the primary strictness points [...].

I don't see how to make sense of that statement. Every part of the program has some meaning, and that meaning is defined by reduction rules, so reduction happens everywhere.

To reduce a function application <expr1> <expr2>, you have to evaluate <expr1> until it has a form like (\x -> <expr1'>) or (getChar >>=) or something else that matches a rule. But for some reason function application doesn't tend to show up on lists of expressions that allegedly "force evaluation", while case always does.

You can see this misconception in a quote from the Haskell wiki, found in another answer:

In practice Haskell is not a purely lazy language: for instance pattern matching is usually strict

I don't understand what could qualify as a "purely lazy language" to whoever wrote that, except, perhaps, a language in which every program hangs because the runtime never does anything. If pattern matching is a feature of your language then you've got to actually do it at some point. To do it, you have to evaluate the scrutinee enough to determine whether it matches the pattern. That's the laziest way to match a pattern that is possible in principle.

~-prefixed patterns are often called "lazy" by programmers, but the language spec calls them "irrefutable". Their defining property is that they always match. Because they always match, you don't have to evaluate the scrutinee to determine whether they match or not, so a lazy implementation won't. The difference between regular and irrefutable patterns is what expressions they match, not what evaluation strategy you're supposed to use. The spec says nothing about evaluation strategies.


main is a strictness point. It is specially designated as the primary strictness point of its context: the program. When the program (main's context) is evaluated, the strictness point of main is activated. [...] Main is usually composed of IO actions, which are also strictness points, whose context is main.

I'm not convinced that any of that has any meaning.

Main's depth is maximal: it must be fully evaluated.

No, main only has to be evaluated "shallowly", to make I/O actions appear at the top level. main is the entire program, and the program isn't completely evaluated on every run because not all code is relevant to every run (in general).

discuss seq and pattern matching in these terms.

I already talked about pattern matching. seq can be defined by rules that are similar to case and application: for example, (\x -> <expr1>) `seq` <expr2> reduces to <expr2>. This "forces evaluation" in the same way that case and application do. WHNF is just a name for what these expressions "force evaluation" to.

Explain the nuances of function application: how is it strict? How is it not?

It's strict in its left expression just like case is strict in its scrutinee. It's also strict in the function body after substitution just like case is strict in the RHS of the selected alternative after substitution.

What about deepseq?

It's just a library function, not a builtin.

Incidentally, deepseq is semantically weird. It should take only one argument. I think that whoever invented it just blindly copied seq with no understanding of why seq needs two arguments. I count deepseq's name and specification as evidence that a poor understanding of Haskell evaluation is common even among experienced Haskell programmers.

let and case statements?

I talked about case. let, after desugaring and type checking, is just a way of writing an arbitrary expression graph in tree form. Here's a paper about it.

unsafePerformIO?

To an extent it can be defined by reduction rules. For example, case unsafePerformIO <expr> of <alts> reduces to unsafePerformIO (<expr> >>= \x -> return (case x of <alts>)), and at the top level only, unsafePerformIO <expr> reduces to <expr>.

This doesn't do any memoization. You could try to simulate memoization by rewriting every unsafePerformIO expression to explicitly memoize itself, and creating the associated IORefs... somewhere. But you could never reproduce GHC's memoization behavior, because it depends on unpredictable details of the optimization process, and because it isn't even type safe (as shown by the infamous polymorphic IORef example in the GHC documentation).

Debug.Trace?

Debug.Trace.trace is just a simple wrapper around unsafePerformIO.

Top-level definitions?

Top-level variable bindings are the same as nested let bindings. data, class, import, and such are a whole different ball game.

Strict data types? Bang patterns?

Just sugar for seq.

Foundry answered 11/12, 2020 at 21:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.