packages feed

lvar-0.2.0.0: src/Data/LVar.hs

-- | @LVar@ is like @Control.Concurrent.STM.TMVar@ but with a capability for
-- listening to its changes.
module Data.LVar
  ( -- * Types
    LVar,

    -- * Creating a LVar
    new,
    empty,

    -- * Modifying a LVar
    get,
    set,
    modify,

    -- * Listening to a LVar
    listenNext,
  )
where

import Control.Concurrent.STM (STM, TMVar, TVar)
import qualified Control.Concurrent.STM as STM
import Control.Monad.IO.Class (MonadIO (..))
import Data.Functor (void)

-- A mutable variable (like @TMVar@), changes to which can be listened to from
-- multiple threads.
data LVar a
  = LVar
      (TMVar a)
      -- ^ A value that changes over time.
      (TVar (TMVar ()))
      -- ^ Gets filled when above gets written to.
      -- Invariant: value is always empty.

-- | Create a new @LVar@ with the given initial value
new :: forall a m. MonadIO m => a -> m (LVar a)
new val = liftIO $ do
  var <- STM.newTMVarIO val
  hole <- STM.newEmptyTMVarIO
  write <- STM.newTVarIO hole
  return $ LVar var write

-- | Like @new@, but there is no initial value. A @get@ will block until an
-- initial value is set using @set@ or @modify@
empty :: MonadIO m => m (LVar a)
empty = liftIO $ do
  var <- STM.newEmptyTMVarIO
  hole <- STM.newEmptyTMVarIO
  write <- STM.newTVarIO hole
  return $ LVar var write

-- | Get the value of the @LVar@
get :: MonadIO m => LVar a -> m a
get (LVar var _) = liftIO $ STM.atomically $ STM.readTMVar var

-- | Set the @LVar@ value; active listeners are automatically notifed.
set :: MonadIO m => LVar a -> a -> m ()
set (LVar var write) val = liftIO $ STM.atomically $ do
  STM.isEmptyTMVar var >>= \case
    True -> STM.putTMVar var val
    False -> void $ STM.swapTMVar var val
  notifyListeners write

-- | Modify the @LVar@ value; active listeners are automatically notified.
modify :: MonadIO m => LVar a -> (a -> a) -> m ()
modify (LVar var write) f = liftIO $ STM.atomically $ do
  curr <- STM.readTMVar var
  void $ STM.swapTMVar var (f curr)
  notifyListeners write

notifyListeners :: TVar (TMVar ()) -> STM ()
notifyListeners write = do
  new_hole <- STM.newEmptyTMVar
  old_hole <- STM.readTVar write
  STM.putTMVar old_hole ()
  STM.writeTVar write new_hole

-- | Listen for the next value update (since the last @listenNext@ or
-- @addListener@).
listenNext :: MonadIO m => LVar a -> m a
listenNext (LVar var write) = liftIO $ do
  hole <- STM.readTVarIO write
  STM.atomically $ do
    () <- STM.readTMVar hole
    STM.readTMVar var