-- |
-- Module      : Data.PVar
-- Copyright   : (c) 2009 Gregory Crosswhite
-- License     : BSD3
-- Maintainer  : Gregory Crosswhite <gcross@phys.washington.edu>
-- Stability   : experimental
-- Portability : ghc
-- Procrastinating variables ('PVar's) are meant to be used in cases where
-- you want to give someone a value that you do not have available yet,
-- but will definitely have ready by the time that they need to use it.
-- 'PVar's have the advantage that you do not make the user of your value
-- execute some kind of unwrapping routine in order to get access to the
-- value within.  For example, this is useful when you are constructing
-- closures that you want to go ahead and construct now even though some
-- of the values that they contain are not available yet.
-- 'PVar's are implemented with a lazy thunk that reads from an
-- 'IORef'; before the 'IORef' is written to, it contains @_|_@ ( in
-- the form of an exception with a descriptive error message) so that
-- an error is raised in the user code if the variable is accidently
-- accessed before the value is ready.
-- NOTE: 'PVar's are modeled closely on the 'IVar' implementation in the
--        ivar-simple package.  The major difference is that if you try
--        to read an 'IVar' before it has been given a value, it blocks
--        until the value is available, whereas reading from a 'PVar'
--        before it is ready raises an exception.  The reason behind the
--        different symantics for 'PVar' is because if the user accidently
--        accesses the value too early, you want there to be a lot of
--        noise to let him or her know about it, rather than merely
--        blocking the thread indefinitely and causing them to wonder
--        what went wrong.

{-# LANGUAGE DeriveDataTypeable #-}

module Data.PVar
    -- * PVar creation
    , newPVar
    , newPVarWithCustomMessage
    -- * Writing to PVars
    , writePVar
    -- * Probing PVars
    , tryReadPVar
    , tryWritePVar
    -- * Errors
    , AccessedTooEarly
    , AlreadyHasAValue
    ) where
import Control.Concurrent.MVar
import Control.Exception
import Control.Monad
import Data.IORef
import Data.Typeable
import System.IO.Unsafe

-- |A procrastinating variable ('PVar' for short).
data PVar a = PVar !(MVar ()) !(IORef a)

-- |The exception raised when a 'PVar' is accessed before it is ready.
data AccessedTooEarly = AccessedTooEarly String deriving (Show, Typeable)
instance Exception AccessedTooEarly

-- |The exception raised by 'writePVar' when one attempts to write to a 'PVar'
--  after it has already been given a value.
data AlreadyHasAValue = AlreadyHasAValue deriving (Show, Typeable)
instance Exception AlreadyHasAValue

-- |Creates a new, empty 'PVar', and returns both a reference you can
--  use to fill the value later as well as a lazy thunk which you can
--  treat as a normal variable;  the latter evaluates to the value
--  stored in the reference if it is available and to bottom otherwise.
newPVar :: IO (PVar a, a)
newPVar = newPVarWithCustomMessage "This procrastinating variable was accessed before it was ready."

-- |Creates a new, empty 'PVar' that raises an exception with a custom
--  message.  (Use this if you want to make explicit to the user of this
--  variable exactly when they should expect its value to become
--  available.)
newPVarWithCustomMessage :: String -> IO (PVar a, a)
newPVarWithCustomMessage message = do
    lock <- newMVar ()
    ref <- newIORef . throw . AccessedTooEarly $ message
    let {-# NOINLINE value #-}
        value = unsafePerformIO $ readIORef ref
    return (PVar lock ref,value)

-- |Tries to read a 'PVar', but does not throw an exception if
-- the value is not ready yet;  instead, if the value is ready it
-- returns @Just value@ and otherwise it returns @Nothing@.
tryReadPVar :: PVar a -> IO (Maybe a)
tryReadPVar (PVar lock ref) = block $ do
    is_empty <- isEmptyMVar lock
    if is_empty
        then readIORef ref >>= return . Just
         else return Nothing

-- |Writes a value to a 'PVar'. Raises an 'AlreadyHasAValue' exception
-- if the 'PVar' already has a value.
writePVar :: PVar a -> a -> IO ()
writePVar pvar value = do
    result <- tryWritePVar pvar value
    unless result $ throwIO AlreadyHasAValue

-- |Attempts to a value to a 'PVar', but instead of throwing an exception
-- it returns 'True' if it was successful and 'False' otherwise.
tryWritePVar :: PVar a -> a -> IO Bool
tryWritePVar (PVar lock ref) value = block $ do
    a <- tryTakeMVar lock
    case a of
        Just _  -> writeIORef ref value >> return True
        Nothing -> return False