cleff-0.3.0.0: src/Cleff/State.hs
{-# LANGUAGE Trustworthy #-}
-- |
-- Copyright: (c) 2021 Xy Ren
-- License: BSD3
-- Maintainer: xy.r@outlook.com
-- Stability: experimental
-- Portability: non-portable (GHC only)
module Cleff.State
( -- * Effect
State (..)
-- * Operations
, get
, put
, state
, gets
, modify
-- * Interpretations
, runState
, runStateIORef
, runStateMVar
, runStateTVar
, zoom
) where
import Cleff
import Cleff.Internal.Base
import Control.Monad (void)
import Data.Atomics (atomicModifyIORefCAS)
import Data.Tuple (swap)
import Lens.Micro (Lens', (&), (.~), (^.))
import UnliftIO.IORef (IORef, newIORef, readIORef, writeIORef)
import UnliftIO.MVar (MVar, modifyMVar, readMVar, swapMVar)
import UnliftIO.STM (TVar, atomically, readTVar, readTVarIO, writeTVar)
-- * Effect
-- | An effect capable of providing a mutable state @s@ that can be read and written. This roughly corresponds to the
-- @MonadState@ typeclass and @StateT@ monad transformer in the @mtl@ library.
data State s :: Effect where
Get :: State s m s
Put :: s -> State s m ()
State :: (s -> (a, s)) -> State s m a
-- * Operations
makeEffect_ ''State
-- | Read the current state.
get :: State s :> es => Eff es s
-- | Update the state with a new value.
put :: State s :> es => s -> Eff es ()
-- | Modify the state /and/ produce a value from the state via a function.
state :: State s :> es
=> (s -> (a, s)) -- ^ The function that takes the state and returns a result value together with a modified state
-> Eff es a
-- | Apply a function to the result of 'get'.
gets :: State s :> es => (s -> t) -> Eff es t
gets = (<$> get)
-- | Modify the value of the state via a function.
modify :: State s :> es => (s -> s) -> Eff es ()
modify f = state (((), ) . f)
-- * Interpretations
handleIORef :: IOE :> es => IORef s -> Handler (State s) es
handleIORef rs = \case
Get -> readIORef rs
Put s' -> writeIORef rs s'
State f -> liftIO $ atomicModifyIORefCAS rs (swap . f)
{-# INLINE handleIORef #-}
-- | Run the 'State' effect.
--
-- === Caveats
--
-- The 'runState' interpreter is implemented with 'Data.IORef.IORef's and there is no way to do arbitrary
-- atomic transactions. The 'state' operation is atomic though and it is implemented with 'atomicModifyIORefCAS', which
-- can be faster than @atomicModifyIORef@ in contention. For any more complicated cases of atomicity, please build your
-- own effect that uses either @MVar@s or @TVar@s based on your need.
--
-- Unlike @mtl@, in @cleff@ the state /will not revert/ when an error is thrown.
--
-- 'runState' will stop taking care of state operations done on forked threads as soon as the main thread finishes its
-- computation. Any state operation done /before main thread finishes/ is still taken into account.
runState :: s -> Eff (State s ': es) a -> Eff es (a, s)
runState s m = thisIsPureTrustMe do
rs <- newIORef s
x <- reinterpret (handleIORef rs) m
s' <- readIORef rs
pure (x, s')
{-# INLINE runState #-}
-- | Run the 'State' effect in terms of operations on a supplied 'IORef'. The 'state' operation is atomic.
--
-- @since 0.2.1.0
runStateIORef :: IOE :> es => IORef s -> Eff (State s ': es) a -> Eff es a
runStateIORef rs = interpret $ handleIORef rs
{-# INLINE runStateIORef #-}
-- | Run the 'State' effect in terms of operations on a supplied 'MVar'.
--
-- @since 0.2.1.0
runStateMVar :: IOE :> es => MVar s -> Eff (State s ': es) a -> Eff es a
runStateMVar rs = interpret \case
Get -> readMVar rs
Put s' -> void $ swapMVar rs s'
State f -> modifyMVar rs \s -> let (x, !s') = f s in pure (s', x)
{-# INLINE runStateMVar #-}
-- | Run the 'State' effect in terms of operations on a supplied 'TVar'.
--
-- @since 0.2.1.0
runStateTVar :: IOE :> es => TVar s -> Eff (State s ': es) a -> Eff es a
runStateTVar rs = interpret \case
Get -> readTVarIO rs
Put s' -> atomically $ writeTVar rs s'
State f -> atomically do
s <- readTVar rs
let (x, !s') = f s
writeTVar rs s'
pure x
{-# INLINE runStateTVar #-}
-- | Run a 'State' effect in terms of a larger 'State' via a 'Lens''.
zoom :: State t :> es => Lens' t s -> Eff (State s ': es) ~> Eff es
zoom field = interpret \case
Get -> gets (^. field)
Put s -> modify (& field .~ s)
State f -> state \t -> let (a, !s) = f (t ^. field) in (a, t & field .~ s)
{-# INLINE zoom #-}