{-# LANGUAGE DeriveDataTypeable #-} {- | 'Sink's are a more flexible alternative to lazy I/O ('unsafeInterleaveIO'). Lazy I/O conflates evaluation with execution; a value obtained from 'unsafeInterleaveIO' can perform side-effects during the evaluation of pure code. Like lazy I/O, a 'Sink' provides a way to obtain the value of the result of an 'IO' action before the action has been executed, but unlike lazy I/O, it does not enable pure code to perform side-effects. Instead, the value is explicitly assigned by a later 'IO' action; repeated attempts to assign the value of a 'Sink' fail. The catch is that this explicit assignment must occur before the value is forced, so just like with lazy I/O, you can't get away with completely ignoring evaluation order without introducing bugs. However, violating this condition does not violate purity because if the value is forced before it has been assigned, it is ⊥. In practice, using 'Sink's instead of 'unsafeInterleaveIO' requires a bit more 'IO' boilerplate. The main practical difference is that while 'unsafeInterleaveIO' requires you to reason about effects from the point of view of pure code, 'Sink's require you to reason about evaluation order of pure code from the point of view of 'IO'; the 'IO' portion of your program will have to be aware of what data is necessary to produce *for* your pure code in order to be able to consume the output it expects *from* your pure code. -} module Data.Sink ( Sink (), newSinkMsg, newSink, tryWriteSink, writeSink , MultipleWrites (..) ) where import Control.Applicative import Control.Exception import Control.Monad import Data.IORef import Data.Maybe import Data.Typeable import System.IO.Unsafe (unsafeInterleaveIO) -- | A write-once reference newtype Sink a = Sink (IORef (Maybe a)) deriving (Eq, Typeable) -- | Create a new 'Sink' and a pure value. If you force the value -- before writing to the 'Sink', the value is ⊥. If you write to the -- 'Sink' before forcing the value, the value will be whatever you -- wrote to the 'Sink'. The 'String' argument is an error message in -- case you force the value before writing to the 'Sink'. newSinkMsg :: String -> IO (Sink a, a) newSinkMsg msg = do ref <- newIORef Nothing x <- unsafeInterleaveIO $ fromMaybe (error msg) <$> readIORef ref return (Sink ref, x) -- | Create a new 'Sink' with a default error message. newSink :: IO (Sink a, a) newSink = newSinkMsg "Evaluated an unwritten sink" -- | Attempt to assign a value to a 'Sink'. If the 'Sink' was -- previously unwritten, write the value and return 'True', otherwise -- keep the old value and return 'False'. This is an atomic (thread -- safe) operation. tryWriteSink :: Sink a -> a -> IO Bool tryWriteSink (Sink ref) x = atomicModifyIORef ref $ maybe (Just x, True) (\y -> (Just y, False)) -- | Attempt to assign a value to a 'Sink'. If the 'Sink' had already -- been written to, throw a 'MultipleWrites' exception. This is an -- atomic (thread safe) operation. writeSink :: Sink a -> a -> IO () writeSink sink x = do success <- tryWriteSink sink x unless success $ throwIO MultipleWrites -- | An exception that is throw by 'writeSink' if you attempt to write -- to a 'Sink' more than once. data MultipleWrites = MultipleWrites deriving (Show, Typeable) instance Exception MultipleWrites