I was just reading your paper and when reading about entangle
and observe
I thought that there was a good chance, that memorization would cause observe
to use the same entangled value twice, thus giving false results.
Indeed I found the following program, where that seems to be the case:
{-# LANGUAGE GADTs #-}
import Test.StrictCheck
import Data.Foldable (traverse_)
import Data.Bifunctor (bimap)
main :: IO ()
main =
(print . bimap prettyDemand (\(x :* Nil) -> prettyDemand x)) `traverse_` [
observe (\() -> ()) (\() -> ()) (),
observe (\() -> ()) (\_ -> ()) ()
]
I would expect the following output:
This matches the actual output for ghc versions 8.2.2 and 8.4.3 with any optimisation level as well as ghc-8.6.1 with -O0
. However when compiling with ghc-8.6.1 and -O1
or -O2
the actual output is this:
Even worse, when we change the order of the two observe
-calls in the list we get this:
I think the best way to fix this would be to change entangle
to this safe (exportable) version:
entangle :: a -> IO (a, IORef (Thunk a))
entangle = do
ref <- newIORef Thunk
return ( unsafePerformIO $ do
writeIORef ref (Eval a)
return a
, ref )
This version of entangle should be referentially transparent and allows the user to observe the evaluation status of the entangled value at different points in time. Furthermore, and most importantly for this issue, leaving out the outer unsafePerformIO
makes sure that two calls to entangle
will create two different entangled values with two different IORef
s, thus making bugs like this impossible. However this of cause would probably require a lot of code to be rewritten to accommodate for the new type and maybe there is a quick and dirty solution that somehow prevents memorization or even not exporting observe and making sure it is only used in safe ways.