{-# LANGUAGE DeriveDataTypeable #-}
-- |
-- Module      : Data.IVar.Simple
-- Copyright   : (c) 2008-2020 Bertram Felgenhauer
-- License     : MIT
--
-- Maintainer  : Bertram Felgenhauer <int-e@gmx.de>
-- Stability   : experimental
-- Portability : ghc
--
-- 'IVar's are write-once variables.
--
-- Similarily to 'MVar's, 'IVar's can be either empty or filled. Once filled,
-- they keep their value indefinitely - they are immutable.
--
-- Reading from an empty 'IVar' will block until the 'IVar' is filled. Because
-- the value read will never change, this is a pure computation.
--
module Data.IVar.Simple (
    IVar,
    new,
    newFull,
    read,
    tryRead,
    write,
    tryWrite,
    BlockedIndefinitelyOnIVar,
) where

import Control.Concurrent.MVar
import Control.Exception
import Control.Monad
import Data.Typeable
import System.IO.Unsafe
import Prelude hiding (read)

-- | A write-once (/immutable/) Variable
data IVar a = IVar (MVar a) a

instance Eq (IVar a) where
    IVar trans1 _ == IVar trans2 _ = trans1 == trans2
    -- note: it would be possible to generalize the type to
    -- eqIVar :: IVar a -> IVar b -> Bool  (but would that be useful?)

-- | Create a new, empty 'IVar'.
new :: IO (IVar a)
new = do
    trans <- newEmptyMVar
    let {-# NOINLINE value #-}
        value = unsafePerformIO $ readMVar trans
    return (IVar trans value)

-- | Create a new filled 'IVar'.
--
-- This is slightly cheaper than creating a new 'IVar' and then writing to it.
newFull :: a -> IO (IVar a)
newFull value = do
    trans <- newMVar (error "unused value")
    return (IVar trans value)

-- | Returns the value of an 'IVar'.
--
-- The evaluation will block until a value is written to the 'IVar' if it
-- has no value yet.
read :: IVar a -> a
read (IVar _ value) = value

-- | Try to read an 'IVar'. Returns 'Nothing' if it has no value yet.
tryRead :: IVar a -> IO (Maybe a)
tryRead (IVar trans value) = do
    empty <- isEmptyMVar trans
    if not empty then return (Just value) else return Nothing

-- | Writes a value to an 'IVar'. Raises a 'BlockedIndefinitelyOnIVar'
-- exception if the variable already has a value.
write :: IVar a -> a -> IO ()
write ivar value = do
    result <- tryWrite ivar value
    when (not result) $ throwIO BlockedIndefinitelyOnIVar
-- Note: It would be easier to block forever when the IVar is full. However,
-- the thread would likely not be garbage collected then.

-- | Writes a value to an 'IVar'. Returns 'True' if successful, and
-- 'False' otherwise.
tryWrite :: IVar a -> a -> IO Bool
tryWrite (IVar trans _) value = tryPutMVar trans value

-- | The thread has attempted to write to a full 'IVar'.
data BlockedIndefinitelyOnIVar = BlockedIndefinitelyOnIVar
    deriving (Typeable)

instance Exception BlockedIndefinitelyOnIVar

instance Show BlockedIndefinitelyOnIVar where
    showsPrec _ BlockedIndefinitelyOnIVar =
        showString "thread blocked indefinitely writing full IVar"