1

I'm trying to understand laziness properly in Haskell. I understand it such that if we have some expression where we do not actually use a sub part of the expression then that sub part will never be evaluated e.g let x = [1..1000] in 0 will never actually evaluate the list but just return 0.

However what if i have something like the following where fib(n) is a fibonacci function and will return an error for n<0

let x = div 100 0 + (20 * 100) division by zero error
let x = fib(-3) + fib(7) n < 0 error

Will (20 * 100) and fib(7) ever get evaluated, or will it wait for the first expression to be computed and then stop after i return an error?

sn3jd3r
  • 496
  • 2
  • 18
  • See https://stackoverflow.com/questions/48384074/understanding-order-of-evaluation-in-haskell. Unless you *force* a particular order (using, for example, a monad), the compiler is free to choose which operand to evaluate first. – chepner Sep 06 '20 at 14:04
  • @chepner How would a monad help here? I think the only way to force a value is to use `seq` or to output it. – Bergi Sep 06 '20 at 15:50
  • 1
    `seq` doesn't actually force an order of evaluation order, either; ``a `seq` b`` simply guarantees that `a` *will* be forced by the time `b` is returned, but it is technically allowed to evaluate `b`, evaluate `a`, *then* return `b`. Regarding monads, isn't `>>=` strict in its first argument, but lazy in the second (so that the effect of the first argument can be used to evaluate the other). – chepner Sep 06 '20 at 16:00
  • See also [How do exceptions work in Haskell?](https://stackoverflow.com/questions/11070690/how-do-exceptions-in-haskell-work). The question appears unrelated, but the excellent answer is very clearly related. – Daniel Wagner Sep 06 '20 at 18:23

2 Answers2

0

As per several comments, the language doesn't make many guarantees about the order of evaluation of subexpressions in a program like:

main = print $ div 100 0 + 20 * 100

So, div 100 0 could be evaluated first and throw an error before 20 * 100 is evaluated, or vice versa. Or, the whole expression could be optimized into unconditionally throwing a division by zero error without evaluating anything, which is what actually happens if you compile it with ghc -O2.

In actual fact, at least with GHC 8.6.5, the function:

foo :: Int -> Int -> Int -> Int
foo x y z = div x y + z * x

compiled with ghc -O2 produces code that attempts the division first and will throw an error if y == 0 before attempting the multiplication, so the subexpressions are evaluated in the order they appear.

HOWEVER, the function with the opposite order:

bar :: Int -> Int -> Int -> Int
bar x y z = z * x + div x y

compiled with ghc -O2 ALSO produces code that tries the division first and will throw an error if y == 0 before attempting the multiplication, so the subexpressions are evaluated in reverse order.

Moreover, even though both versions try the division before the multiplication, there's still a difference in their evaluation order -- bar fully evaluates z before trying the division, while foo evaluates the division before fully evaluating z, so if a lazy, error-generating value is passed for z, these two functions will produce different behavior. In particular,

main = print $ foo 1 0 (error "not so fast")

throws a division by zero error while:

main = print $ bar 1 0 (error "not so fast")

says "not so fast". Neither attempts the multiplication, though.

There aren't any simple rules here. The only way to see these differences is to compile with flags that dump intermediate compiler output, like:

ghc -ddump-stg -dsuppress-all -dsuppress-uniques -fforce-recomp -O2 Test.hs

and inspect the generated code.

If you want to guarantee a particular evaluation order, you need to write something like:

import Control.Parallel (pseq)

foo' :: Int -> Int -> Int -> Int
foo' x y z = let a = div x y
                 b = z * x
             in  a `pseq` b `pseq` a + b

bar' :: Int -> Int -> Int -> Int
bar' x y z = let a = z * x
                 b = div x y
             in  a `pseq` b `pseq` a + b

The function pseq is similar to the seq function discussed in the comments. The seq function would work here but doesn't always guarantee an evaluation order. The pseq function is supposed to provide a guaranteed order.

If your actual goal is to understand Haskell's lazy evaluation, rather than prevent specific subexpressions from being evaluated in case of errors in other subexpressions, then I'm not sure that looking at these examples will help much. Instead, taking a look at this answer to a related question already linked in the comments may give you better sense of how laziness "works" conceptually.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
-1

In this case expressions (20 * 100) and fib(7) will evaluation, but it is because the operator (+) firstly evaluate its second argument. If you write, for example, (20 * 100) + div 100 0, the part (20 * 100) won't evaluate. You can on your own detect which argument evaluate firstly: (error "first") + (error "second"), for example.

SergeyKuz1001
  • 875
  • 1
  • 4
  • 6
  • 5
    I'm not sure you can rely on one or the other being evaluated first. `(+)` is strict in both arguments, but there's nothing that *requires* it try to evaluate one or the other argument first. – chepner Sep 06 '20 at 13:45
  • 1
    See also [A Semantics for Imprecise Exceptions](https://www.microsoft.com/en-us/research/publication/a-semantics-for-imprecise-exceptions/). `(+)` is explicitly called out as one of the operations that is permitted to return either argument if both are exceptions. – Daniel Wagner Sep 06 '20 at 18:21