epass-0.2.1: src/Control/Concurrent/Mailbox.hs
{- | This module provides Erlang like functionality for message passing.
Instead of mailboxes attached to each process you have to create the needed
mailboxes yourself. This means that messages cannot be send to processes
or threads directly, but only to mailboxes. On the other hand multiple
threads may share a mailbox and one thread may have multiple mailboxes.
For a simple example on how to receive messages have a look at the
'MsgHandler' type.
-}
module Control.Concurrent.Mailbox
(
-- * Mailbox
MailboxClass (..)
, Mailbox
, newMailbox
-- * Sending messages
, send
, (<!)
-- * Receiving messages
, receive
, receiveNonBlocking
, receiveTimeout
-- * Message handlers
, MsgHandler
, Handler
, handler
-- * Message handler combinators
, (.>)
, (<|>)
)
where
import Prelude hiding (catch)
import Control.Concurrent (yield)
import Control.Concurrent.STM
import Control.Exception hiding (Handler)
import Data.Time
import System.Timeout
-- Mailbox ---------------------------------------------------------------------
{- | Any instance of 'MailboxClass' may be used as a mailbox for message
passing. @b@ is the mailbox type and m is the message type.
-}
class MailboxClass b where
-- | Get a message from the mailbox (with 'Mailbox' it is the first one).
getMessage
:: b m -- ^ the mailbox
-> IO m -- ^ the message
{- | Put a message back to the mailbox (with 'Mailbox' it will be placed
at the beginning of the mailbox).
-}
unGetMessage
:: b m -- ^ the mailbox
-> m -- ^ the message
-> IO ()
{- | Add a new message to the mailbox (with 'Mailbox' it will be placed at
the end of the mailbox).
-}
putMessage
:: b m -- ^ the mailbox
-> m -- ^ the message
-> IO ()
-- | Checks wether the mailbox is empty.
isEmpty
:: b m -- ^ the mailbox
-> IO Bool -- ^ 'True' if empty
{- | Call this function to cleanup before exit or when the mailbox is no
longer needed.
-}
close
:: b m
-> IO ()
-- | A 'Chan' based mailbox.
newtype Mailbox m = MBox (TChan m)
-- | Creates a new mailbox.
newMailbox :: IO (Mailbox m)
newMailbox = fmap MBox newTChanIO
instance MailboxClass Mailbox where
getMessage (MBox chan) = atomically $ readTChan chan
unGetMessage (MBox chan) msg = atomically $ unGetTChan chan msg
putMessage (MBox chan) msg = atomically $ writeTChan chan msg
isEmpty (MBox chan) = atomically $ isEmptyTChan chan
close _ = return ()
-- Sending messages ------------------------------------------------------------
-- | Send the given message to the given mailbox.
send
:: MailboxClass b
=> b m -- ^ the mailbox
-> m -- ^ the message
-> IO ()
send mbox msg = do
putMessage mbox msg
yield
-- | An alias for 'send' in the flavor of Erlang's @!@.
(<!)
:: MailboxClass b
=> b m
-> m
-> IO ()
(<!) = send
-- Timeout calculations (internal) ---------------------------------------------
timeoutFactor
:: Num a
=> a
timeoutFactor = 1000000
calcEndTime
:: Int
-> IO UTCTime
calcEndTime to = do
curTime <- getCurrentTime
let dt = fromIntegral to / timeoutFactor
return $ addUTCTime dt curTime
calcTimeLeft
:: UTCTime
-> IO Int
calcTimeLeft endTime = do
curTime <- getCurrentTime
return $ round $ (diffUTCTime endTime curTime) * timeoutFactor
-- Receiving messages ----------------------------------------------------------
{- | Receive messages in the flavour of Erlang's @receive@.
For each message in the mailbox all message handlers are matched until a
matching message is found. It will be removed from the mailbox and the
matching message handler's action will be performed.
If no message matches any of the message handler, 'receive' will block and
check new incoming messages until a match is found.
-}
receive
:: MailboxClass b
=> b m -- ^ mailbox to receive on
-> [MsgHandler m a] -- ^ message handlers
-> IO a
receive _ [] = error "No message handler given! Cannot match."
receive mbox handlers = do
a <- matchAll mbox handlers
a
{- | Like 'receive', but times out after a given time. In case of timeout the
timeout handler is executed.
-}
receiveTimeout
:: MailboxClass b
=> b m -- ^ the mailbox
-> Int -- ^ timeout in us
-> [MsgHandler m a] -- ^ message handlers
-> IO a -- ^ timeout handler
-> IO a
receiveTimeout _ _ [] toa = toa
receiveTimeout mbox 0 handlers toa = receiveNonBlocking mbox handlers toa
receiveTimeout mbox to handlers toa = do
endTime <- calcEndTime to
ma <- matchAllTimeout mbox endTime handlers
case ma of
Just a -> a
Nothing -> toa
{- | Like 'receive', but doesn't block. If no match was found, the default
handler is executed.
-}
receiveNonBlocking
:: MailboxClass b
=> b m -- ^ the mailbox
-> [MsgHandler m a] -- ^ message handlers
-> IO a -- ^ default handler
-> IO a
receiveNonBlocking mbox handlers na = do
ma <- matchCurrent mbox handlers
case ma of
Just a -> a
Nothing -> na
-- Matching messages (internal) ------------------------------------------------
data TimeoutResult a = Match (IO a)
| NoMatch
| Timeout
matchAll
:: MailboxClass b
=> b m
-> [MsgHandler m a]
-> IO (IO a)
matchAll mbox hs = do
m <- getMessage mbox
ma <- match m hs
case ma of
Just a ->
return a
Nothing -> do
r <- matchAll mbox hs
unGetMessage mbox m
return r
matchAllTimeout
:: MailboxClass b
=> b m
-> UTCTime
-> [MsgHandler m a]
-> IO (Maybe (IO a))
matchAllTimeout mbox endTime hs = do
timeLeft <- calcTimeLeft endTime
if timeLeft <= 0
then return Nothing
else do
mm <- timeout timeLeft $ getMessage mbox
case mm of
Just m -> do
matched <- matchTimeout m endTime hs
case matched of
NoMatch -> do
r <- matchAllTimeout mbox endTime hs
unGetMessage mbox m
return r
(Match a) ->
return $ Just a
Timeout -> do
unGetMessage mbox m
return Nothing
Nothing ->
return Nothing
matchCurrent
:: MailboxClass b
=> b m
-> [MsgHandler m a]
-> IO (Maybe (IO a))
matchCurrent mbox hs = do
empty <- isEmpty mbox
if empty
then return Nothing
else do
m <- getMessage mbox
ma <- match m hs
case ma of
Just a ->
return $ Just a
Nothing -> do
r <- matchCurrent mbox hs
unGetMessage mbox m
return r
match
:: m
-> [MsgHandler m a]
-> IO (Maybe (IO a))
match _ [] = return Nothing
match m (h : hs) = do
ma <- catch (case h m of (Handler a) -> return $ Just a)
handlePatternMatchFail
case ma of
Just action -> return $ Just action
Nothing -> match m hs
matchTimeout
:: m
-> UTCTime
-> [MsgHandler m a]
-> IO (TimeoutResult a)
matchTimeout _ _ [] = return NoMatch
matchTimeout m endTime (h : hs) = do
timeLeft <- calcTimeLeft endTime
if timeLeft <= 0
then return Timeout
else do
ma <- timeout timeLeft $
catch (case h m of (Handler a) -> return $ Just a)
handlePatternMatchFail
case ma of
Just (Just action) -> return $ Match action
Just Nothing -> matchTimeout m endTime hs
Nothing -> return $ Timeout
handlePatternMatchFail
:: PatternMatchFail
-> IO (Maybe (IO a))
handlePatternMatchFail _ = return Nothing
-- Message handlers ------------------------------------------------------------
{- | A function that matches a given message and returns the corresponding
handler.
In case of an pattern matching error 'receive' will continue matching
the next 'MsgHandler' / message.
For example you may write somthing like this:
> receive mbox
> [ \ True -> handler $ return 1
> , \ False -> handler $ return 2
> ]
-}
type MsgHandler m a = m -> Handler a
-- | The action to perfom in case of successful matching.
data Handler a = Handler (IO a)
-- | Generate a handler from an 'IO' action.
handler
:: IO a
-> Handler a
handler = Handler
-- Message handler combinators -------------------------------------------------
-- | Apply a function to the result of an message handler.
(.>)
:: MsgHandler m a -- ^ message handler
-> (a -> b) -- ^ function
-> MsgHandler m b -- ^ new message handler
(h .> f) m =
let Handler a = h m
in handler $ a >>= return . f
{- | Combine to lists of message handlers into one list. The results of the
message handler will be wrapped in 'Either'.
-}
(<|>)
:: [MsgHandler m a] -- ^ message handlers
-> [MsgHandler m b] -- ^ more message handlers
-> [MsgHandler m (Either a b)] -- ^ combined message handlers
has <|> hbs = (map (.> Left) has) ++ (map (.> Right) hbs)