11

Just as the title says: what guarantees are there for a Haskell function returning unit to be evaluated? One would think that there is no need to run any kind of evaluation in such a case, the compiler could replace all such calls with an immediate () value unless explicit requests for strictness are present, in which case the code might have to decide whether it should return () or bottom.
I have experimented with this in GHCi, and it seems like the opposite happens, that is, such a function appears to be evaluated. A very primitive example would be

f :: a -> ()
f _ = undefined

Evaluating f 1 throws an error due to the presence of undefined, so some evaluation definitely happens. It is not clear how deep the evaluation goes, though; sometimes it appears to go as deep as it is necessary to evaluate all calls to functions returning (). Example:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

This code loops indefinitely if presented with g (let x = 1:x in x). But then

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

can be used to show that h (f 1) returns (), so in this case not all unit-valued subexpressions are evaluated. What is the general rule here?

ETA: of course I know about laziness. I'm asking what prevents compiler writers from making this particular case even lazier than usually possible.

ETA2: summary of the examples: GHC appears to treat () exactly as any other type, i.e. as if there was a question about which regular value inhabiting the type should be returned from a function. The fact that there's only one such value does not seem to be (ab)used by the optimization algorithms.

ETA3: when I say Haskell, I mean Haskell-as-defined-by-the-Report, not Haskell-the-H-in-GHC. Seems to be an assumption not shared as widely as I imagined (which was 'by 100% of the readers'), or I would probably have been able to formulate a clearer question. Even so, I regret changing the title of the question, as it originally asked what guarantees are there for such a function being called.

ETA4: it would seem that this question has run its course, and I'm considering it unanswered. (I was looking for a 'close question' function but only found 'answer your own question' and as it cannot be answered, I did not go down that route.) No one brought up anything from the Report that would decide it either way, which I'm tempted to interpret as a strong but not definite 'no guarantee for the language as such' answer. All we know is that the current GHC implementation will not skip the evaluation of such a function.

I've run into the actual problem when porting an OCaml app to Haskell. The original app had a mutually recursive structure of many types, and the code declared a number of functions called assert_structureN_is_correct for N in 1..6 or 7, each of which returned unit if the structure was indeed correct and threw an exception if it was not. In addition, these functions called each other as they decomposed the correctness conditions. In Haskell this is better handled using the Either String monad, so I transcribed it that way, but the question as a theoretical issue remained. Thanks for all the inputs and replies.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Kryptozoon
  • 133
  • 6
  • 1
    This is laziness at work. Unless the result of a function is demanded (e.g. by pattern matching against a constructor), the body of the function is not evaluated. To observe the difference, try comparing `h1::()->() ; h1 () = ()` and `h2::()->() ; h2 _ = ()`. Run both `h1 (f 1)` and `h2 (f 1)`, and see that only the first one demands `(f 1)`. – chi Nov 27 '19 at 13:53
  • Sure, but my question is mapped to your example as follows: why does f 1 get called at all in either case? Laziness would seem to dictate that it gets replaced with () without any sort of evaluation happening. ETA: actually, why do h1 and h2 get called, while we are at it? – Kryptozoon Nov 27 '19 at 13:57
  • 2
    "Laziness would seem to dictate that it gets replaced with () without any sort of evaluation happening." What does that mean? `f 1` is "replaced" by `undefined` in all cases. – oisdk Nov 27 '19 at 14:05
  • 4
    A function `... -> ()` can 1) terminate and return `()`, 2) terminate with an exception/runtime error and fail to return anything, or 3) diverge (infinite recursion). GHC does not optimize the code assuming only 1) can happen: if `f 1` is demanded, it does not skip its evaluation and return `()`. The Haskell semantics is to evaluate it and see what happens among 1,2,3. – chi Nov 27 '19 at 14:10
  • 2
    There's really nothing special about `()` (either the type or the value) in this question. All the same observations happen if you replace `() :: ()` with, say, `0 :: Int` everywhere. These are all just boring old consequences of lazy evaluation. – Daniel Wagner Nov 27 '19 at 15:49
  • It would appear that Haskell semantics is to avoid returning bottom if avoidable, by means of laziness. Examples abound where a 'strict Haskell' would return bottom and the actual, lazy Haskell just avoids the trap if it is not necessary to evaluate all subexpressions, i.e. Haskell absolutely does not try to figure out if there's a way to return bottom instead of some regular value. Therefore it could be argued that it should not do so here but return immediately the only regular value that could be the result of this computation. – Kryptozoon Nov 27 '19 at 15:50
  • @DanielWagner: how is it a consequence of lazy evaluation that the evaluation of f 1 is not lazy enough? – Kryptozoon Nov 27 '19 at 16:00
  • 3
    no, "avoiding" etc. is not the Haskell semantics. and there are *two* possible values of `()` type, `()` and `undefined`. – Will Ness Nov 27 '19 at 16:04
  • @WillNess: laziness implies avoiding returning bottom if avoidable. Not trying to evaluate that which is not needed means returning a regular value in situations where strict evaluation would return bottom. ETA: and `f 1` does not need to evaluate anything to return a regular value. – Kryptozoon Nov 27 '19 at 16:09
  • @WillNess: I thought a bit more about laziness vs. bottom, and came to the conclusion that not only is avoiding returning bottom a crucial part of lazy evaluation, but that the _only_ clear reason for laziness, indeed its raison d'etre is to avoid some cases when in a strict semantics bottom would be returned. My main reason for this is that I know of no other semantic difference between laziness and strictness but that some expressions that would run into bottom in a strict semantics will return a regular value in lazy semantics, while the opposite never happens. – Kryptozoon Nov 28 '19 at 13:55
  • does the new evidence from the Report in my answer leaves something still in doubt? discovering whether a value is `⊥` or not means actually evaluating it to WHNF. – Will Ness Jan 21 '20 at 07:32
  • @WillNess: of course if pattern matching commences, the `_|_` will get a chance to shine through, and that's what your Report chapter and verse supports. The question however is why does pattern matching start at all in this case instead of ignoring the equation and promptly returning `()`. The closest I saw in the Report was the Translation section in 4.4.3.1 Function bindings (not quoted bc of length), but even that feels like falling short from mandating the evaluation of pattern matching, describing instead how it should proceed, if at all. – Kryptozoon Jan 21 '20 at 12:41
  • [`putStrLn :: String -> IO ()`](https://www.haskell.org/onlinereport/haskell2010/haskellch7.html#x14-1430007.1) in `putStrLn (case (undefined :: ()) of () -> "()")` demands the **`String`** value from the `case` expression, the evaluation of which is governed by what I quoted in the answer. No expression has no `()` to return into any context here. You want/expect/fear the `case (undefined :: ()) of () -> "()"` expression returning `"()"` string on its own, but that's not what the rules which I quoted are saying. Chapter 4 just says that clausal definitions are equivalent to case expressions. – Will Ness Jan 21 '20 at 15:11
  • here's another example for you. `foo a = ((\x -> a) y)` is an invalid function definition because it contains a reference to an undefined variable `y` (assuming this definition does not appear inside a scope where `y` happens to be defined). Trying to define it produces an error *``"Not in scope: `y' "``*. One could say, why should that be the case? `y`'s value isn't used anyway, why should it being undefined impede the workings of this lambda function? but, it's - not - how - the - Haskell - language - happens - to - be - defined. /end of example/ – Will Ness Mar 27 '20 at 15:13

1 Answers1

9

You appear to come from the assumption that the type () has only one possible value, (), and thus expect that any function call returning a value of type () should automatically be assumed to indeed produce the value ().

This is not how Haskell works. Every type has one more value in Haskell, namely no value, error, or so called "bottom", encoded by undefined. Thus an evaluation is actually taking place:

main = print (f 1)

is equivalent to the Core language's

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

or even(*)

main = _Case (f 1) _Of x -> putStr "()"

and Core's _Case is forcing:

"Evaluating a %case [expression] forces the evaluation of the expression being tested (the “scrutinee”). The value of the scrutinee is bound to the variable following the %of keyword, ...".

The value is forced to weak head normal form. This is part of the language definition.

Haskell is not a declarative programming language.


(*) print x = putStr (show x) and show () = "()", so the show call can be compiled away altogether.

The value is indeed known in advance as (), and even the value of show () is known in advance as "()". Still the accepted Haskell semantics demand that the value of (f 1) is forced to weak head normal form before proceeding to print that known in advance string, "()".


edit: Consider concat (repeat []). Should it be [], or should it be an infinite loop?

A "declarative language"'s answer to this is most probably []. Haskell's answer is, the infinite loop.

Laziness is "poor man's declarative programming", but it still isn't the real thing.

edit2: print $ h (f 1) == _Case (h (f 1)) _Of () -> print () and only h is forced, not f; and to produce its answer h doesn't have to force anything, according to its definition h _ = ().

parting remarks: Laziness may have raison d'etre but it's not its definition. Laziness is what it is. It is defined as all values being initially thunks which are forced to WHNF according to the demands coming from main. If it helps avoid bottom in a certain specific case according to its specific circumstances, then it does. If not, not. That is all.

It helps to implement it yourself, in your favorite language, to get a feel for it. But we can also trace evaluation of any expression by carefully naming all interim values as they come into being.


Going by the Report, we have

f :: a -> ()
f = \_ -> (undefined :: ())

then

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

and with

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

it continues

 = putStrLn (case (undefined :: ()) of () -> "()")

Now, section 3.17.3 Formal Semantics of Pattern Matching of the Report says

The semantics of case expressions [are given] in Figures 3.1–3.3. Any implementation should behave so that these identities hold [...].

and case (r) in Figure 3.2 states

(r)     case ⊥ of { K x1 … xn -> e; _ -> e′ } =  ⊥
        where K is a data constructor of arity n 

() is data constructor of arity 0, so it is the same as

(r)     case ⊥ of { () -> e; _ -> e′ } =  ⊥

and the overall result of the evaluation is thus .

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • @DanielWagner I had in mind the `case` from Core actually, and was ignoring the gaping hole. :) I've edited to mention the Core. – Will Ness Nov 27 '19 at 14:51
  • 1
    Wouldn't the forcing be in `show` invoked by `print`? Something like `show x = case x of () -> "()"` – user253751 Nov 27 '19 at 15:10
  • @user253751 seems likely that it is so. I guess I treated `print` as a given, with its semantics being "show the value". my "Core translation" is just an illustration. wherever specifically this forcing happens, it *happens*. that's the point I'm making. – Will Ness Nov 27 '19 at 15:12
  • @user253751 if we think in terms of `putStr . show $ f 1`, we could still say it is `putStr` that *really* demands the value of `show`. – Will Ness Nov 27 '19 at 15:19
  • case cannot be forcing in general, as you seem to imply, or `case undefined of ~x -> ()` would fail but it doesn't. If you mean that this particular `case` is forcing because of the refutable pattern, then we are back to the question of why the notion of 'forced' does not include the shortcut 'replace f _ with (), no questions asked'. And the Haskell literature is choke-full of examples where returning bottom is avoided due to laziness; I guess I don't understand, then, why did you bring it up. – Kryptozoon Nov 27 '19 at 15:43
  • 1
    I refer to `case` in Core, not in Haskell itself. Haskell is translated into Core, which has a forcing `case`, AFAIK. You are correct that `case` in Haskell isn't forcing by itself. I could write something in Scheme or ML (if I could write ML that is), or pseudocode. – Will Ness Nov 27 '19 at 15:59
  • @WillNess: okay, I did not know that. So if I wrote a program - I'm not going to, because it would be ugly Haskell, but assuming I did - that relied upon calls to unit-returning functions not being optimized away, I'd be safe from rogue compiler writers introducing this kind of optimization in a later version because the standard is implicitly/cumulatively against it? – Kryptozoon Nov 27 '19 at 16:05
  • 1
    The authoritative answer to all of this is probably somewhere in the Report. All I know is there's no "optimization" going on here, and "regular value" is not a meaningful term in this context. Whatever is forced, forced. `print` forces as much as needed to print. it doesn't look at the type, the types are gone, erased, by the time the program runs, the correct printing subroutine is already chosen and compiled in, according to the type, at compile time; that subroutine is still going to force its input value to WHNF at run time, and if it was undefined, it'll cause an error. – Will Ness Nov 27 '19 at 16:19
  • @Kryptozoon, if you are writing to the standard alone, then you are absolutely *not* safe. The standard only gives a denotational semantics -- it could be run on a room of mathematicians if we could find the funding. – luqui Nov 27 '19 at 16:41
  • @luqui do you mean the Report? I could only find [one appearance](https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-880004.5) of the word "semantics" in the Report's [TOC](https://www.haskell.org/onlinereport/haskell2010/). – Will Ness Nov 27 '19 at 16:48
  • @WillNess yes, one of these days I should at least skim the thing so I don't go around parroting things I have heard about it like this lol – luqui Nov 27 '19 at 18:22
  • @luqui though that was only in reference to the TOC. [Chapter 3](https://www.haskell.org/onlinereport/haskell2010/haskellch3.html) has lots of occurrences of the word. :) I've also hit on [this page](https://downloads.haskell.org/~ghc/7.6.3/docs/html/users_guide/informal-semantics.html) talking about Core. – Will Ness Nov 27 '19 at 19:54
  • I feel like the translation to Core is confusing here because it implies semantics which are different from some that the question asks about. It seems to imply that `(f 1)` would be forced to WHNF before being passed to `print` but I don’t think that happens. The last part of the question has `h _ = ()` and points out that `h (f 1)` is not undefined, so surely it wouldn’t look like `case f 1 of x -> h x` in Core (I’m assuming here you want to imply that x is forced to WHNF). I think the reason `print (f 1)` fails is that the implementation of `show` for `()` forces its argument. – Dan Robertson Nov 27 '19 at 23:20
  • @DanRobertson `print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()` would be the translation. I'm not saying it *is*, I'm saying it is equivalent to what is. If we're unclear about Haskell's semantics, surely a code in Haskell won't clarify much, will be subject to the same uncertainties! I could use "`FORCE`" as a pseudocode: `{print} (FORCE {h (f 1)})` is what I intend, where `{g}` means "a compiled `g`". yes `print == putStr . show` and `{print} == {putStr . show}` most probably, but that's a detail. `show` doesn't force it on its own, that is driven by `putStr` which is driven by RTS. – Will Ness Nov 28 '19 at 07:31
  • (contd.) maybe it's all compiled away, who knows. Most probably it is; I'm practically sure there's no full `show` implementation in there, but rather the specific subroutine already expecting the `()` argument! but as an illustration I feel it's equivalent. Perhaps I ought to change `case` to `_Case` in the answer. (another answer could compile to Core and explore the output, but then, another version of compiler could compile the code differently). – Will Ness Nov 28 '19 at 07:37
  • Consider two classes for printing: `class P1 a where { p1 :: a -> IO () }` and `class P2 a where { p2 :: a -> IO () }` and different implementations for unit: `instance P1 () where p1 _ = putStr "()"` and `instance P2 () where p2 () = putStr "()"`. We should find that `main = p1 (f 1)` runs fine but with `p2` it will fail with undefined. The key difference is that the forcing of the unit value (ie the case in Core) happens only inside `p2 :: () -> IO ()`. No forcing happens in `main` between creating the think for `f 1` and calling `p1` or `p2`. Your answer implies it always happens. – Dan Robertson Nov 28 '19 at 09:19
  • @luqui: I'm tending towards agreeing with you on this. The Report leaves this issue open, or the relevant chapter and verse would already have been quoted ITT (not to mention I'd probably have found it before/instead of posting the question), so the implementers are apparently free to do away with all calls to functions returning unit types, including e.g. `data MyUnit = MyUnitValue`, except so far they didn't. I'd start at least throwing warnings for functions returning unit types (= terminal objects in the Hask category) if I was the GHC developer team. – Kryptozoon Nov 28 '19 at 13:47
  • @DanRobertson I've edited to say it is "equivalent". i.e. it doesn't say that's what happens, just that it's equivalent to what happens. I've also added another possible variant, `main = _Case (f 1) _Of x -> putStr "()"`, which is a valid translation and has the same semantics as the original `print (f 1)`, so serves as a valid illustration. In no way have I claimed that it is what is actually happening. the snippet only serves to illustrate the overall process: the value of `(f 1)` is forced to WHNF, and `"()"` is slated to be printed after that. – Will Ness Nov 28 '19 at 16:44