lattest-lib-0.1.1: src/Lattest/Streams/Synchronized.hs
{-# LANGUAGE TupleSections #-}
module Lattest.Streams.Synchronized (
TInputStream,
read,
readAll,
unRead,
hasInput,
fromInputStreamBuffered,
makeTInputStream,
fromBuffer,
fromTMVar,
duplicate,
map,
mapUnbuffered,
mergeBuffered,
mergeBufferedWith,
consumeBufferedWith,
Streamed(..),
tryRead,
tryReadIO,
tryReadIO'
)
where
import Prelude hiding (read, map)
import Control.Concurrent.STM(STM, retry)
import Control.Concurrent.STM.TQueue(TQueue, newTQueueIO, writeTQueue, readTQueue, isEmptyTQueue, unGetTQueue)
import Control.Concurrent.STM.TMVar(TMVar, takeTMVar, isEmptyTMVar)
import Control.Concurrent.STM.TVar(newTVarIO, readTVar, writeTVar)
import Control.Exception(throwIO, Exception)
import Control.Monad (void, when)
import Data.List(singleton)
import GHC.Conc (atomically, forkIO, ThreadId)
import System.IO.Streams (InputStream, OutputStream, makeOutputStream, connect)
import qualified System.IO.Streams as Streams (write)
import Control.Monad.Extra((||^))
data TInputStream a = TInputStream {
tRead :: STM (Maybe a),
tUnRead :: a -> STM (),
tHasInput :: STM Bool -- whether this stream has a (Maybe a) available right now, *or* whether it is closed. Note: it may be tempting
-- to omit this field, and just check whether tRead retries, but that is ambiguous: tRead will retry either if
-- there is no input yet, *or* if someone is writing to the unknown TVars used to implement tRead. The first case
-- would not have an input, the second case would.
}
read :: TInputStream a -> STM (Maybe a)
read = tRead
hasInput :: TInputStream a -> STM Bool
hasInput = tHasInput
-- read inputs from the stream until it has no more input
readAll :: TInputStream a -> STM [Maybe a]
readAll tis = reverse <$> readAll' []
where
readAll' acc = do
readAttempt <- tryRead tis
case readAttempt of
StreamClosed -> return (Nothing:acc)
Unavailable -> return acc
Available next -> readAll' (Just next:acc)
unRead :: a -> TInputStream a -> STM ()
unRead = flip tUnRead
data Streamed a = Available a | Unavailable | StreamClosed deriving (Eq, Ord, Show, Read)
tryRead :: TInputStream a -> STM (Streamed a)
tryRead tis = do
ready <- hasInput tis
if ready
then do
minput <- read tis
return $ maybe StreamClosed Available minput
else return Unavailable
tryReadIO :: TInputStream a -> IO (Streamed a)
tryReadIO = atomically . tryRead
data TInputStreamClosedException = TInputStreamClosedException deriving (Eq, Ord, Show)
instance Exception TInputStreamClosedException
tryReadIO' :: TInputStream a -> IO (Maybe a)
tryReadIO' tis = do
streamed <- tryReadIO tis
case streamed of
Available a -> return $ Just a
Unavailable -> return Nothing
StreamClosed -> throwIO TInputStreamClosedException
makeTInputStream :: STM (Maybe a) -> STM Bool -> IO (TInputStream a)
makeTInputStream readInput hasInput' = do
pushbackStack <- newTQueueIO
withClosedState $ TInputStream {
tRead = do
hasPushback <- isNonEmptyTQueue pushbackStack
if hasPushback
then Just <$> readTQueue pushbackStack
else readInput,
tUnRead = unGetTQueue pushbackStack,
tHasInput = isNonEmptyTQueue pushbackStack ||^ hasInput'
}
withClosedState :: TInputStream a -> IO (TInputStream a)
withClosedState stream = do
closeVar <- newTVarIO False
return $ TInputStream {
tRead = do
closed <- readTVar closeVar
if closed
then return Nothing
else do
mi <- tRead stream
case mi of
Just i -> return $ Just i
Nothing -> do
writeTVar closeVar True
return Nothing,
tUnRead = tUnRead stream,
tHasInput = do
closed <- readTVar closeVar
if closed
then return True
else tHasInput stream
}
fromTMVar :: TMVar (Maybe a) -> IO (TInputStream a)
fromTMVar tmvar = makeTInputStream (takeTMVar tmvar) (not <$> isEmptyTMVar tmvar)
-- note: the buffer is also used to push back values. Thus, do not read this buffer from outside the resulting stream, as then this stream may
-- violate the principle that pushed back values are read again.
fromBuffer :: TQueue (Maybe a) -> IO (TInputStream a)
fromBuffer buffer = withClosedState $ TInputStream {
tRead = readTQueue buffer,
tUnRead = unGetTQueue buffer . Just,
tHasInput = isNonEmptyTQueue buffer
}
-- a synchronized input stream that reads from the given input stream. A monitoring thread reads the given input stream and buffers the inputs on the background
-- TODO support a bound for the buffer
fromInputStreamBuffered :: InputStream a -> IO (ThreadId, TInputStream a)
fromInputStreamBuffered is = do
buffer <- newTQueueIO
bufferOS <- makeOutputStream $ atomically . writeTQueue buffer -- an intermediate output stream to write to the queue
threadid <- forkIO $ connect is bufferOS -- move items from the original adapter into the buffer in a separate thread
(threadid,) <$> fromBuffer buffer
mapUnbuffered :: (a -> b) -> (b -> a) -> TInputStream a -> TInputStream b -- we need an inverse to push a's back to the original TInputStream of b's
mapUnbuffered f f' tis = TInputStream { -- FIXME either remove or wrap in withClosedState
tRead = fmap f <$> read tis,
tUnRead = tUnRead tis . f',
tHasInput = tHasInput tis
}
map :: (a -> b) -> TInputStream a -> IO (TInputStream b)
map f tis = makeTInputStream (fmap f <$> read tis) (hasInput tis)
-- Take inputs from both given input streams. If both streams have an input available, the first stream takes precedence. The resulting stream closes
-- if either given stream closes. The merge is buffered, so that the sequence of arrival of inputs in both streams is preserved, even if the resulting
-- stream is not read.
mergeBuffered :: TInputStream a -> TInputStream a -> IO (TInputStream a)
mergeBuffered = mergeBufferedWith $ \tis1 tis2 -> singleton <$> do
ready1 <- hasInput tis1
if ready1
then read tis1
else do
ready2 <- hasInput tis2
if ready2
then read tis2
else retry
-- Take inputs from both given input streams, using the given combinator function. The merge is buffered, so that the sequence of arrival of inputs in both streams is preserved,
-- even if the resulting stream is not read. The resulting stream closes if either given stream closes.
mergeBufferedWith :: (TInputStream a -> TInputStream a -> STM [Maybe a]) -> TInputStream a -> TInputStream a -> IO (TInputStream a)
mergeBufferedWith merge tis1 tis2 = consumeBufferedWith (merge tis1 tis2) (hasAnyInput tis1 tis2) -- FIXME if one stream closes, then no further inputs should be read from the other stream, and only Nothings should be returned instead
hasAnyInput :: TInputStream a -> TInputStream a -> STM Bool
hasAnyInput tis1 tis2 = hasInput tis1 ||^ hasInput tis2
-- Actively take inputs from the given list STM, via a separate thread. This ensures that ordering from the given list STM is preserved as much as possible,
-- in case of concurrent inputs. The resulting stream only reads via the separate thread if needed, and reads from the list STM directly if this is faster.
-- The resulting stream closes if the consumer closes. This function assumes that the given STMs close properly, i.e., no Just's are sent after a Nothing
consumeBufferedWith :: STM [Maybe a] -> STM Bool -> IO (TInputStream a)
consumeBufferedWith producer producerHasInput = do
buffer <- newTQueueIO
let writeToBuffer = writeTQueue buffer
--bufferOS <- makeOutputStream $ \x -> putStrLn "Synchronized.merging" >> (atomically . writeTQueue buffer) x -- an intermediate output stream to write to the queue
void $ forkIO $ bufferLoop writeToBuffer -- move items from the original streams into the buffer in a separate thread
bufferIS <- fromBuffer buffer
return $ bufferIS {
tRead = pickOneAndMergeRest buffer writeToBuffer,
tHasInput = hasInput bufferIS ||^ producerHasInput
}
where
bufferLoop writeToBuffer = do
continue <- atomically $ do
produced <- producer
writeSeqTo produced writeToBuffer
when continue $ bufferLoop writeToBuffer
writeSeqTo [] _ = return True -- TODO this is an OutputStream utils function, similar to Streams.write/writeTo, move to an appropriate module
writeSeqTo (Nothing:_) writeToBuffer = do
_ <- writeToBuffer Nothing
return False
writeSeqTo (Just a:as) writeToBuffer = do
_ <- writeToBuffer (Just a)
writeSeqTo as writeToBuffer
pickOneAndMergeRest buffer writeToBuffer = do
bufferHasInput <- not <$> isEmptyTQueue buffer
produced <- if bufferHasInput
then singleton <$> readTQueue buffer
else producer
case produced of
[] -> retry
(Just x:xs) -> do
void $ writeSeqTo xs writeToBuffer
return $ Just x
(Nothing:_) -> do
unGetTQueue buffer Nothing
return Nothing
{-
-- TODO either remove or expose as nice util function, even though we don't seem to need this now
tconnect :: TInputStream a -> OutputStream a -> IO ()
tconnect tis os = tconnect'
where
tconnect' = do
mA <- atomically $ read tis
case mA of
Nothing -> Streams.write Nothing os
Just _ -> do
Streams.write mA os
tconnect'
-}
-- FIXME split to a separate module, conceptually there's nothing synchronized about this w.r.t. other OutputStreams
duplicate :: OutputStream a -> OutputStream a -> IO (OutputStream a)
duplicate out1 out2 = makeOutputStream $ \maybeVal -> do
Streams.write maybeVal out1
Streams.write maybeVal out2
isNonEmptyTQueue :: TQueue a -> STM Bool
isNonEmptyTQueue queue = not <$> isEmptyTQueue queue