What is the connection between laziness and purity?
Asked Answered
W

3

45

Laziness is what kept Haskell pure. If it had been strict, purity would soon go out the window.

I fail to see the connection between the evaluation strategy of a language and its purity. Considering the reputation of the tweet's author I am most certainly overlooking something. Maybe someone can shed some light.

Wooton answered 18/8, 2020 at 13:15 Comment(4)
Writing "impure lazy code" is incredibly difficult, because there are no guarantees what order your side effects will happen in. Having commited to a lazy evaluation strategy removes the temptation to let impurity creap in, and forced early Haskell developers to invent pure functional patterns for working with side effects, like Monadic IOFurnary
The modern assessment of this "temptation" should be that it's overblown. Neither Idris nor Purescript had to deal with any such thing, as strict pure languages.Changeup
In a followup tweet, the author explained that "If a language is lazy it normally has to be pure because order of evaluation in a lazy language is hard to predict which makes managing side effects even more difficult than normal."Typographer
related, apparently.Proceleusmatic
N
35

You're right, from a modern POV this doesn't really make sense. What is true is that lazy-by-default would make reasoning about side-effectful code a nightmare, so lazyness does require purity – but not the other way around.

What does require lazyness though is the way Haskell in versions 1.0–1.2, following its predecessor Miranda, emulated IO without monads. Short of any explicit notion of side-effect sequencing, the type of executable programs was

main :: [Response] -> [Request]

which would, for a simple interactive program, work something like this: main would at first just ignore its input list. So thanks to lazyness, the values in that list wouldn't actually need to exist, at that point. In the meantime it would produce a first Request value, e.g. a terminal prompt for the user to type something. What was typed would then come back as a Response value, which only now actually needed to be evaluated, giving rise to a new Request, etc. etc..

https://www.haskell.org/definition/haskell-report-1.0.ps.gz

In version 1.3 they then switched to the monadic-IO interface that we all know and love today, and at that point lazyness wasn't really necessary anymore. But before that, common wisdom was that the only way to interact with the real world without lazyness was to allow side-effectful functions, thus the statement that without lazyness, Haskell would just have gone down the same path as Lisp and ML before it.

Necrology answered 18/8, 2020 at 13:42 Comment(2)
I'm not sure what the first paragraph is saying. Laziness => purity, therefore if we commit to making Haskell lazy, we commit to making it pure. If Haskell were strict, then there is no requirement that Haskell be impure, but language implementors would likely give in to the temptation and start allowing impurity, because it is possible. The statement is a comment about us humans, not just on the design of the language itself.Ulema
@Ulema Are languages designed and used by robots?Blent
S
28

There are two aspects to this tweet: first, the fact that from a technical point of view, laziness generally mandates purity; second, the fact that from a practical point of view, strictness could still allow purity, but in practice it usually doesn't (i.e., with strictness, purity "goes out the window").

Simon Peyton-Jones explains both of these aspects in the paper "A History of Haskell: Being Lazy With Class". With respect to the technical aspect, in the section 3.2 Haskell is Pure, he writes (with my bold emphasis):

An immediate consequence of laziness is that evaluation order is demand-driven. As a result, it becomes more or less impossible to reliably perform input/output or other side effects as the result of a function call. Haskell is, therefore, a pure language.

If you can't see why laziness makes impure effects unreliable, I'm sure it's because you're over-thinking it. Here's a simple example illustrating the problem. Consider a hypothetical impure function that reads some information from a configuration file, namely some "basic" configuration and some "extended" configuration whose format depends on the configuration file version information in the header:

getConfig :: Handle -> Config
getConfig h =
  let header = readHeader h
      basic = readBasicConfig h
      extended = readExtendedConfig (headerVersion header) h
  in Config basic extended

where readHeader, readBasicConfig, and readExtendedConfig are all impure functions that sequentially read bytes from the file (i.e., using typical, file pointer-based sequential reads) and parse them into the appropriate data structures.

In a lazy language, this function probably can't work as intended. If the header, basic, and extended variable values are all lazily evaluated, then if the caller forces basic first followed by extended, the effects will be called in order readBasic, readHeader, readExtendedConfig; while if the caller forces extended first followed by basic, the effects will be called in order readHeader, readExtendedConfig, readBasic. In either case, bytes intended to be parsed by one function will be parsed by another.

And, these evaluation orders are gross oversimplifications which assume that the effects of the sub-functions are "atomic" and that readExtendedConfig reliably forces the version argument for any access to extended. If not, depending on which parts of basic and extended are forced, the order of the (sub)effects in readBasic, readExtendedConfig, and readHeader may be reordered and/or intermingled.

You can work around this specific limitation by disallowing sequential file access (though that comes with some significant costs!), but similar unpredictable out-of-order effect execution will cause problems with other I/O operations (how do we ensure that a file updating function reads the old contents before truncated the file for update?), mutable variables (when exactly does that lock variable get incremented?), etc.

With respect to the practical aspect (again with my bold emphasis), SPJ writes:

Once we were committed to a lazy language, a pure one was inescapable. The converse is not true, but it is notable that in practice most pure programming languages are also lazy. Why? Because in a call-by-value language, whether functional or not, the temptation to allow unrestricted side effects inside a "function" is almost irresistible.

...

In retrospect, therefore, perhaps the biggest single benefit of laziness is not laziness per se, but rather that laziness kept us pure, and thereby motivated a great deal of productive work on monads and encapsulated state.

In his tweet, I believe Hutton is referring not to the technical consequence of laziness leading to purity, but rather the practical consequence of strictness tempting the language designers to relax purity "just in this one, special case", after which purity quickly goes out the window.

Suribachi answered 18/8, 2020 at 15:54 Comment(1)
This. Haskell implementers even gave in to the temptation and did allow unrestricted side effects inside a computation through unsafePerformIO - but lazyness is what kept everyone away from actually utilising it to build impure programs from these, as they could not rely on evaluation order.Waly
B
17

The other answers give the historical context that's most likely what that comment is referring to. I think the connection runs even deeper.

Eager languages, even those that call themselves "pure", do not have referential transparency in as strong a sense as in Haskell:

let f = E in
\x -> f x

is not equivalent to

\x -> E x

if the former expression is evaluated eagerly and the evaluation of E diverges.

Eager languages require a distinction between values and computations: variables are only substituted with values, but expressions stand for computations, which is why the "obvious" reduction of let above is not valid. An expression being more than the value it denotes is exactly what it means for a language to be effectful. In this very technical sense, an eager language such as Purescript (the first example I could think of in this space) is not pure.

Purescript is pure when we ignore non-termination and order of evaluation, which is what virtually every programmer does, so that deserves credit.

In contrast, in a lazy language, the distinction between values and computations is blurred. Everything denotes a value, even non-terminating expressions, which are "bottom". You can substitute all you want without worrying about what expressions do. That, in my opinion, is the point of purity.

One could argue that, actually, we could also say that a diverging expression in an eager language denotes bottom and it's just that let denotes a strict function. Let's be honest, it might be a good explanation a posteriori, but nobody thinks that way except Haskellers and programming language terrorists.

Bivalve answered 18/8, 2020 at 15:55 Comment(3)
I assume in your last line you actually meant "programming language theorists", but I can think of a few people I might call "programming language terrorists"!Malleable
You're raising a valid point with your example, but the practical relevance is limited because Haskell actually has almost the same problem despite being lazy: in let f=E in \x->f x, the E will only be evaluated once while in \x->E x it will be evaluated over and over again at each function call. It's not quite as obvious as with the ⊥s you get in the strict case, but still quite important for programmers to be aware of. And actually it's not that much of a problem to choose the better way of writing it for each occurence.Necrology
You can argue that it's just a technicality from the point of view of operational semantics, but I think the fact that it no longer concerns purely functional correctness deeply affects how you reason about programs, and how compilers optimize them. So I dispute that the practical relevance is limited. Delaying computations with fun () -> is a necessity to even express many eager programs. In Haskell, yes you have to be aware of operational concerns to fully understand how something runs, but it doesn't show up in the syntax as often, that further encourages denotational thinking.Bivalve

© 2022 - 2024 — McMap. All rights reserved.