exception - Si un thunk resulta en una excepción, ¿se mantiene la excepción como resultado del thunk?
haskell lazy-evaluation (1)
Permítame responder a esta pregunta mostrando cómo GHC realmente hace esto, usando la ghc-heap-view
. Probablemente puedas reproducir esto con ghc-vis
y obtener buenas fotos.
Comienzo creando una estructura de datos con un valor de excepción en algún lugar:
Prelude> :script /home/jojo/.cabal/share/ghc-heap-view-0.5.1/ghci
Prelude> let x = map ((1::Int) `div`) [1,0]
Al principio es puramente un thunk (que parece involucrar varias clases de tipos):
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)()
Ahora evalúo las partes que no lanzan excepciones:
Prelude> (head x, length x)
(1,2)
Prelude> System.Mem.performGC
Prelude> :printHeap x
[I# 1,_thunk (_fun (I# 1)) (I# 0)]
El segundo elemento de la lista sigue siendo solo un procesador "normal". Ahora evalúo esto, obtengo una excepción y la miro de nuevo:
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())]
Puede ver que ahora es un procesador que hace referencia a un objeto SomeException
. El constructor de datos SomeException
tiene el tipo forall e . Exception e => e -> SomeException
forall e . Exception e => e -> SomeException
, por lo que el segundo parámetro del constructor es el constructor DivideByZero
de la excepción ArithException
, y el primer parámetro es la instancia de clase de tipo Exception
correspondiente.
Este truco se puede pasar como cualquier otro valor de Haskell y, si se evalúa, volverá a generar la excepción. Y, al igual que cualquier otro valor, la excepción se puede compartir:
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)
Las mismas cosas suceden con hilos y MVars, nada especial allí.
Creé este pequeño programa que crea un procesador de larga duración que finalmente falla con una excepción. Luego, múltiples hilos intentan evaluarlo.
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
Aumentar el número de subprocesos claramente no tiene impacto en el cálculo, lo que sugiere que un thunk fallido mantiene la excepción como resultado. ¿Es verdad? ¿Está este comportamiento documentado / especificado en algún lugar?
Actualización: Cambiando la línea forkFinally
a
forkFinally io (/e -> print e >> putMVar mvar ())
confirma que cada hilo falla con la excepción.