If a thunk results in an exception, is the exception kept as the result of the thunk?
Asked Answered
B

1

16

I created this small program that creates a long-running thunk that eventually fails with an exception. Then, multiple threads try to evaluate it.

import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar

main = do
    let thunk = let p = product [1..10^4]
                 in if p `mod` 2 == 0 then error "exception"
                                      else ()
    children <- replicateM 2000 (myForkIO (print thunk))
    mapM_ takeMVar children

-- | Spawn a thread and return a MVar which can be used to wait for it.
myForkIO :: IO () -> IO (MVar ())
myForkIO io = do
     mvar <- newEmptyMVar
     forkFinally io (\_ -> putMVar mvar ())
     return mvar

Increasing the number of threads has clearly no impact on the computation, which suggests that a failed thunk keeps the exception as the result. Is it true? Is this behavior documented/specified somewhere?

Update: Changing the forkFinally line to

forkFinally io (\e -> print e >> putMVar mvar ())

confirms that each thread fails with the exception.

Bellbottoms answered 2/8, 2013 at 13:35 Comment(6)
The exception is the value of the expression. What else could evaluating the expression multiple times do?Selfwinding
@Selfwinding That's what I suspect, but I want to be sure. It could also try recompute the value again and again.Bellbottoms
I do know GHC internals, otherwise I could not create tools like ghc-heap-view, so I am not sure what more do you need. Can you please clarify your question if my answer is not helpful enough?Juttajutty
@JoachimBreitner Your answer is helpful and I upvoted it immediately. Your answer seemed to be based on observation (and I didn't know you create ghc-heap-view), and what I'd like to see is that some GHC hacker says something like "Yes, GHC always works this way, this is its guaranteed behavior." If there will be no such answer, I'll gladly award the bounty to yours.Bellbottoms
There are no such guarantees on the language level. I’m quite confident that GHC always works this way – but are there guarantees? I don’t think GHC guarantees anything about the evaluation of pure terms and is free to duplicate and computation there.Juttajutty
Yay, earned my first bounty on SO :-).Juttajutty
J
12

Let me answer this question by showing how GHC actually does this, using the ghc-heap-view library. You can probably reproduce this with ghc-vis and get nice pictures.

I start by creating a data structure with an exception value somewhere:

Prelude> :script /home/jojo/.cabal/share/ghc-heap-view-0.5.1/ghci 
Prelude> let x = map ((1::Int) `div`) [1,0]

At first it is purely a thunk (that seems to involve various type classes):

Prelude> :printHeap x
let f1 = _fun
in (_bco [] (_bco (D:Integral (D:Real (D:Num _fun _fun _fun _fun _fun _fun _fun) (D:Ord (D:Eq _fun _fun) _fun _fun _fun _fun _fun _fun _fun) _fun) (D:Enum _fun _fun f1 f1 _fun _fun _fun _fun) _fun _fun _fun _fun _fun _fun _fun) _fun) _fun)()

Now I evaluate the non-exception-throwing-parts:

Prelude> (head x, length x)
(1,2)
Prelude> System.Mem.performGC
Prelude> :printHeap x
[I# 1,_thunk (_fun (I# 1)) (I# 0)]

The second element of the list is still just a “normal” thunk. Now I evaluate this, get an exception, and look at it again:

Prelude> last x
*** Exception: divide by zero
Prelude> System.Mem.performGC
Prelude> :printHeap x
[I# 1,_thunk (SomeException (D:Exception _fun (D:Show _fun _fun _fun) _fun _fun) DivideByZero())]

You can see it is now a thunk that references an SomeException object. The SomeException data constructor has type forall e . Exception e => e -> SomeException, so the second parameter of the constructor is the DivideByZero constructor of the ArithException exception, and the first parameter the corresponding Exception type class instance.

This thunk can be passed around just like any other Haskell value and will, if evaluated, raise the exception again. And, just like any other value, the exception can be shared:

Prelude> let y = (last x, last x)
Prelude> y
(*** Exception: divide by zero
Prelude> snd y
*** Exception: divide by zero
Prelude> System.Mem.performGC
Prelude> :printHeap y
let x1 = SomeException (D:Exception _fun (D:Show _fun _fun _fun) _fun _fun) DivideByZero()
in (_thunk x1,_thunk x1)

The same things happen with threads and MVars, nothing special there.

Juttajutty answered 3/8, 2013 at 21:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.