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.

Petr
  • 62,528
  • 13
  • 153
  • 317
  • 1
    The exception *is* the value of the expression. What else could evaluating the expression multiple times do? – Carl Aug 02 '13 at 20:33
  • @Carl That's what I suspect, but I want to be sure. It could also try recompute the value again and again. – Petr Aug 03 '13 at 06:07
  • 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? – Joachim Breitner Aug 06 '13 at 21:34
  • @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. – Petr Aug 07 '13 at 06:18
  • 1
    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. – Joachim Breitner Aug 07 '13 at 21:24
  • Yay, earned my first bounty on SO :-). – Joachim Breitner Aug 13 '13 at 08:32

1 Answers1

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.

Joachim Breitner
  • 25,395
  • 6
  • 78
  • 139