{- | 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 , () , (<|>) ) where import Prelude hiding (catch) import Control.Concurrent 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 { unMBox :: Chan m } -- | Creates a new mailbox. newMailbox :: IO (Mailbox m) newMailbox = fmap MBox newChan instance MailboxClass Mailbox where getMessage = readChan . unMBox unGetMessage = unGetChan . unMBox putMessage = writeChan . unMBox isEmpty = isEmptyChan . unMBox close = const $ 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 @!@. ( b m -> m -> IO () ( 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)