pipes-text-1.0.0: Pipes/Prelude/Text.hs
{-# LANGUAGE RankNTypes #-}
module Pipes.Prelude.Text
( -- * Simple line-based Text IO
-- $lineio
fromHandleLn,
toHandleLn,
stdinLn,
stdoutLn,
stdoutLn',
readFileLn,
writeFileLn,
)
where
import Control.Exception (throwIO, try)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Foreign.C.Error (Errno (Errno), ePIPE)
import qualified GHC.IO.Exception as G
import Pipes
import Pipes.Safe (MonadSafe (..))
import qualified Pipes.Safe.Prelude as Safe
import qualified System.IO as IO
import Prelude hiding (readFile, writeFile)
-- $lineio
-- Line-based operations are marked with a final \-@Ln@, like 'stdinLn', 'readFileLn', etc.
-- They are drop-in 'Text' replacements for the corresponding 'String' operations in
-- @Pipes.Prelude@ and @Pipes.Safe.Prelude@ - a final \-@Ln@ being added where necessary.
-- This module can thus be imported unqualified if @Pipes.Prelude@ is imported qualified, as
-- it must be.
--
-- In using the line-based operations, one is producing and consuming semantically significant individual texts,
-- understood as lines, just as one would produce or pipe 'Int's or 'Char's or anything else.
-- The standard materials from @Pipes@ and @Pipes.Prelude@ and
-- @Data.Text@ are all you need to work with them, and
-- you can use these operations without using any of the other modules in this package.
--
-- Thus, to take a trivial case, here we upper-case three lines from standard input and write
-- them to a file. (@runSafeT@ from @Pipes.Safe@ just makes sure to close any handles opened in its scope;
-- it is only needed for @readFileLn@ and @writeFileLn@.)
--
-- >>> import Pipes
-- >>> import qualified Pipes.Prelude as P
-- >>> import qualified Pipes.Prelude.Text as Text
-- >>> import qualified Data.Text as T
-- >>> Text.runSafeT $ runEffect $ Text.stdinLn >-> P.take 3 >-> P.map T.toUpper >-> Text.writeFileLn "threelines.txt"
-- one<Enter>
-- two<Enter>
-- three<Enter>
-- >>> :! cat "threelines.txt"
-- ONE
-- TWO
-- THREE
--
-- The point of view is very much that of @Pipes.Prelude@, substituting @Text@ for @String@.
-- It would still be the same even if
-- we did something a bit more sophisticated, like run an ordinary attoparsec 'Text' parser on
-- each line, as is frequently desirable. Here we use
-- a minimal attoparsec number parser, @scientific@, on separate lines of standard input,
-- dropping bad parses with @P.concat@:
--
-- >>> import Data.Attoparsec.Text (parseOnly, scientific)
-- >>> P.toListM $ Text.stdinLn >-> P.takeWhile (/= "quit") >-> P.map (parseOnly scientific) >-> P.concat
-- 1<Enter>
-- 2<Enter>
-- bad<Enter>
-- 3<Enter>
-- quit<Enter>
-- [1.0,2.0,3.0]
--
-- The line-based operations are, however, subject to a number of caveats.
--
-- * Where these line-based operations read from a handle, they will
-- accumulate indefinitely long lines. This makes sense for input
-- typed in by a user, and for locally produced files of known characteristics, but
-- otherwise not. See the post on
-- <http://www.haskellforall.com/2013/09/perfect-streaming-using-pipes-bytestring.html perfect streaming>
-- to see why @pipes-bytestring@ and this package, outside this module, take a different approach, in which
-- lines themselves are permitted to stream without accumulation.
--
-- * The line-based operations,
-- like those in @Data.Text.IO@, use the system encoding (and @T.hGetLine@, @T.hPutLine@ etc.)
-- and thus are slower than the \'official\' route, which would use the very fast
-- bytestring IO operations from @Pipes.ByteString@ and the
-- encoding and decoding functions in @Pipes.Text.Encoding@, which are also quite fast
-- thanks to the @streaming-commons@ package.
--
-- * The line-based operations (again like those in @Data.Text.IO@) will
-- generate text exceptions after the fashion of
-- @Data.Text.Encoding@, rather than returning the undigested bytes in the
-- style of @Pipes.Text.Encoding@. This is the standard practice in the pipes libraries.
-- | Read separate lines of 'Text' from 'IO.stdin' using 'T.getLine', terminating on end of input.
--
-- This function will accumulate indefinitely long strict 'Text's. See the caveats above.
stdinLn :: MonadIO m => Producer' T.Text m ()
stdinLn = fromHandleLn IO.stdin
{-# INLINEABLE stdinLn #-}
-- | Write 'Text' lines to 'IO.stdout' using 'putStrLn', terminating without error on a broken output pipe
stdoutLn :: MonadIO m => Consumer' T.Text m ()
stdoutLn = go
where
go = do
str <- await
x <- liftIO $ try (T.putStrLn str)
case x of
Left
G.IOError
{ G.ioe_type = G.ResourceVanished,
G.ioe_errno = Just ioe
}
| Errno ioe == ePIPE ->
return ()
Left e -> liftIO (throwIO e)
Right () -> go
{-# INLINEABLE stdoutLn #-}
-- | Write lines of 'Text' to 'IO.stdout'. This does not handle a broken output pipe,
-- but has a polymorphic return value.
stdoutLn' :: MonadIO m => Consumer' T.Text m r
stdoutLn' = for cat (liftIO . T.putStrLn)
{-# INLINE [1] stdoutLn' #-}
{-# RULES
"p >-> stdoutLn'" forall p.
p >-> stdoutLn' =
for p (liftIO . T.putStrLn)
#-}
-- | Read separate lines of 'Text' from a 'IO.Handle' using 'T.hGetLine',
-- terminating at the end of input
--
-- This operation will accumulate indefinitely large strict texts. See the caveats above.
fromHandleLn :: MonadIO m => IO.Handle -> Producer' Text m ()
fromHandleLn h = go
where
getLine :: IO (Either G.IOException Text)
getLine = try (T.hGetLine h)
go = do
txt <- liftIO getLine
case txt of
Left _ -> return ()
Right y -> do
yield y
go
{-# INLINEABLE fromHandleLn #-}
-- to do: investigate differences from the above:
-- fromHandleLn :: MonadIO m => IO.Handle -> Producer' T.Text m ()
-- fromHandleLn h = go
-- where
-- go = do
-- eof <- liftIO $ IO.hIsEOF h
-- unless eof $ do
-- str <- liftIO $ T.hGetLine h
-- yield str
-- go
-- {-# INLINABLE fromHandleLn #-}
-- | Write separate lines of 'Text' to a 'IO.Handle' using 'T.hPutStrLn'
toHandleLn :: MonadIO m => IO.Handle -> Consumer' T.Text m r
toHandleLn handle = for cat (liftIO . T.hPutStrLn handle)
{-# INLINE [1] toHandleLn #-}
{-# RULES
"p >-> toHandleLn handle" forall p handle.
p >-> toHandleLn handle =
for p (liftIO . T.hPutStrLn handle)
#-}
-- | Stream separate lines of text from a file. Apply @runSafeT@ after running the
-- pipeline to manage the opening and closing of the handle.
--
-- This operation will accumulate indefinitely long strict text chunks.
-- See the caveats above.
readFileLn :: MonadSafe m => FilePath -> Producer Text m ()
readFileLn file = Safe.withFile file IO.ReadMode fromHandleLn
{-# INLINE readFileLn #-}
-- | Write lines to a file. Apply @runSafeT@ after running the
-- pipeline to manage the opening and closing of the handle.
writeFileLn :: (MonadSafe m) => FilePath -> Consumer' Text m r
writeFileLn file = Safe.withFile file IO.WriteMode toHandleLn
{-# INLINEABLE writeFileLn #-}