box-0.9.0: src/Box/IO.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
-- | IO effects
module Box.IO
( fromStdin,
toStdout,
fromStdinN,
toStdoutN,
readStdin,
showStdout,
refCommitter,
refEmitter,
handleE,
handleC,
fileE,
fileC,
fileEText,
fileEBS,
fileCText,
fileCBS,
logConsoleC,
logConsoleE,
pauser,
changer,
quit,
restart,
)
where
import Box.Codensity
import Box.Committer
import Box.Connectors
import Box.Emitter
import Control.Concurrent.Async
import Control.Exception
import Control.Monad.State.Lazy
import Data.Bool
import Data.ByteString.Char8 as Char8
import Data.Foldable
import Data.Function
import Data.Functor.Contravariant
import Data.IORef
import qualified Data.Sequence as Seq
import Data.String
import Data.Text as Text hiding (null)
import Data.Text.IO as Text
import System.IO as IO
import Prelude
-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Prelude
-- >>> import Box
-- >>> import Data.Bool
-- >>> import Data.Text (Text, pack)
-- >>> import Data.Functor.Contravariant
-- * console
-- | Emit text from stdin
--
-- @
-- λ> emit fromStdin
-- hello
-- Just "hello"
-- @
fromStdin :: Emitter IO Text
fromStdin = Emitter $ Just <$> Text.getLine
-- | Commit to stdout
--
-- >>> commit toStdout ("I'm committed!" :: Text)
-- I'm committed!
-- True
toStdout :: Committer IO Text
toStdout = Committer $ \a -> Text.putStrLn a >> pure True
-- | Finite console emitter
--
-- @
-- λ> toListM /<$|/> fromStdinN 2
-- hello
-- hello again
-- ["hello","hello again"]
-- @
fromStdinN :: Int -> CoEmitter IO Text
fromStdinN n = source n Text.getLine
-- FIXME: This doctest sometimes fails with the last value not being printed. Hypothesis: the pipe collapses before the console print effect happens.
-- | Finite console committer
--
-- > glue <$> contramap (pack . show) <$> (toStdoutN 2) <*|> qList [1..3]
-- 1
-- 2
toStdoutN :: Int -> CoCommitter IO Text
toStdoutN n = sink n Text.putStrLn
-- | Read from console, throwing away read errors
--
-- λ> glueN 2 showStdout (readStdin :: Emitter IO Int)
-- 1
-- 1
-- hippo
-- 2
-- 2
readStdin :: Read a => Emitter IO a
readStdin = witherE (pure . either (const Nothing) Just) . readE $ fromStdin
-- | Show to stdout
--
-- >>> glue showStdout <$|> qList [1..3]
-- 1
-- 2
-- 3
showStdout :: Show a => Committer IO a
showStdout = contramap (Text.pack . show) toStdout
-- | Emits lines of Text from a handle.
-- handleEText = handleE Text.hGetLine
-- handleEBS = handleE Char8.hGetLine
-- | Emits lines of Text from a handle.
handleE :: (IsString a, Eq a) => (Handle -> IO a) -> Handle -> Emitter IO a
handleE action h = Emitter $ do
l :: (Either IOException a) <- try (action h)
pure $ case l of
Left _ -> Nothing
Right a -> bool (Just a) Nothing (a == "")
-- | Commit lines of Text to a handle.
handleC :: (Handle -> a -> IO ()) -> Handle -> Committer IO a
handleC action h = Committer $ \a -> do
action h a
pure True
-- | Commit lines of Text to a handle.
-- handleCBS = handleC Char8.hPutStrLn
-- | Emits lines of Text from a handle.
-- handleCText = handleC Text.hPutStrLn
-- | Emit lines of Text from a file.
fileE :: FilePath -> BufferMode -> IOMode -> (Handle -> Emitter IO a) -> CoEmitter IO a
fileE fp b m action = Codensity $ \eio ->
withFile
fp
m
( \h -> do
hSetBuffering h b
eio (action h)
)
fileEText :: FilePath -> BufferMode -> CoEmitter IO Text
fileEText fp b = fileE fp b ReadMode (handleE Text.hGetLine)
fileEBS :: FilePath -> BufferMode -> CoEmitter IO ByteString
fileEBS fp b = fileE fp b ReadMode (handleE Char8.hGetLine)
-- | Commit lines of Text to a file.
fileC :: FilePath -> IOMode -> BufferMode -> (Handle -> Committer IO a) -> CoCommitter IO a
fileC fp m b action = Codensity $ \cio ->
withFile
fp
m
( \h -> do
hSetBuffering h b
cio (action h)
)
fileCText :: FilePath -> BufferMode -> IOMode -> CoCommitter IO Text
fileCText fp m b = fileC fp b m (handleC Text.hPutStrLn)
fileCBS :: FilePath -> BufferMode -> IOMode -> CoCommitter IO ByteString
fileCBS fp m b = fileC fp b m (handleC Char8.hPutStrLn)
-- | Commit to an IORef
--
-- >>> (c1,l1) <- refCommitter :: IO (Committer IO Int, IO [Int])
-- >>> glue c1 <$|> qList [1..3]
-- >>> l1
-- [1,2,3]
refCommitter :: IO (Committer IO a, IO [a])
refCommitter = do
ref <- newIORef Seq.empty
let c = Committer $ \a -> do
modifyIORef ref (Seq.:|> a)
pure True
let res = toList <$> readIORef ref
pure (c, res)
-- | Emit from a list IORef
--
-- >>> e <- refEmitter [1..3]
-- >>> toListM e
-- [1,2,3]
refEmitter :: [a] -> IO (Emitter IO a)
refEmitter xs = do
ref <- newIORef xs
let e = Emitter $ do
as <- readIORef ref
case as of
[] -> pure Nothing
(x : xs') -> do
writeIORef ref xs'
pure $ Just x
pure e
-- | simple console logger for rough testing
logConsoleE :: Show a => String -> Emitter IO a -> Emitter IO a
logConsoleE label e = Emitter $ do
a <- emit e
Prelude.putStrLn (label <> show a)
pure a
-- | simple console logger for rough testing
logConsoleC :: Show a => String -> Committer IO a -> Committer IO a
logConsoleC label c = Committer $ \a -> do
Prelude.putStrLn (label <> show a)
commit c a
-- | Pause an emitter based on a Bool emitter
pauser :: Emitter IO Bool -> Emitter IO a -> Emitter IO a
pauser b e = Emitter $ fix $ \rec -> do
b' <- emit b
case b' of
Nothing -> pure Nothing
Just False -> emit e
Just True -> rec
-- | Create an emitter that indicates when another emitter has changed.
changer :: (Eq a) => a -> Emitter IO a -> CoEmitter IO Bool
changer a0 e = evalEmitter a0 $ Emitter $ do
r <- lift $ emit e
case r of
Nothing -> pure Nothing
Just r' -> do
r'' <- get
put r'
pure (Just (r' == r''))
-- | quit a process based on a Bool emitter
--
-- > quit <$> speedEffect (pure 2) <$> (resetGap 5) <*|> pure io
-- 0
-- 1
-- 2
-- 3
-- 4
-- Left True
quit :: Emitter IO Bool -> IO a -> IO (Either Bool a)
quit flag io = race (checkE flag) io
checkE :: Emitter IO Bool -> IO Bool
checkE e = fix $ \rec -> do
a <- emit e
-- atomically $ check (a == Just False)
case a of
Nothing -> pure False
Just True -> pure True
Just False -> rec
-- | restart a process if flagged by a Bool emitter
restart :: Emitter IO Bool -> IO a -> IO (Either Bool a)
restart flag io = fix $ \rec -> do
res <- quit flag io
case res of
Left True -> rec
Left False -> pure (Left False)
Right r -> pure (Right r)