Если thunk приводит к исключению, исключение сохраняется в результате thunk?

Я создал эту небольшую программу, которая создает долговременный thunk, который в конечном итоге терпит неудачу с исключением. Затем несколько потоков пытаются оценить его.

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

Увеличение количества потоков явно не влияет на вычисление, что говорит о том, что неудавшийся thunk сохраняет исключение в качестве результата. Это правда? Является ли это поведение документированным/указано где-то?

Обновление: Изменение строки forkFinally на

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

подтверждает, что каждый поток выходит из строя с исключением.

Ответы

Ответ 1

Позвольте мне ответить на этот вопрос, показывая, как GHC на самом деле это делает, используя библиотеку ghc-heap-view. Вероятно, вы можете воспроизвести это с помощью ghc-vis и получить хорошие снимки.

Я начинаю с создания структуры данных со значением исключения где-то:

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

Вначале это просто бит (который, как представляется, включает в себя различные классы типов):

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)()

Теперь я оцениваю части, не содержащие исключения:

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

Второй элемент списка - это всего лишь "нормальный" удар. Теперь я оцениваю это, получаю исключение и снова смотрю на него:

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())]

Вы можете видеть, что теперь это thunk, который ссылается на объект SomeException. Конструктор данных SomeException имеет тип forall e . Exception e => e -> SomeException, поэтому вторым параметром конструктора является конструктор DivideByZero ArithException исключение, а первый параметр - соответствующий экземпляр класса Exception.

Этот thunk может быть передан так же, как и любое другое значение Haskell, и, если он будет оцениваться, снова вызовет исключение. И, как и любое другое значение, исключение может быть общим:

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)

То же самое происходит с потоками и MVars, ничего особенного там.