Подтвердить что ты не робот

Как реализовать "кошку" в Haskell?

Я пытаюсь написать простую программу cat в Haskell. Я хотел бы взять несколько имен файлов в качестве аргументов и записать каждый файл последовательно STDOUT, но моя программа печатает только один файл и завершает работу.

Что мне нужно сделать, чтобы мой код печатал каждый файл, а не только первый, который прошел?

import Control.Monad as Monad
import System.Exit
import System.IO as IO
import System.Environment as Env

main :: IO ()
main = do
    -- Get the command line arguments
    args <- Env.getArgs

    -- If we have arguments, read them as files and output them
    if (length args > 0) then catFileArray args

    -- Otherwise, output stdin to stdout
    else catHandle stdin

catFileArray :: [FilePath] -> IO ()
catFileArray files = do
    putStrLn $ "==> Number of files: " ++ (show $ length files)
    -- run `catFile` for each file passed in
    Monad.forM_ files catFile

catFile :: FilePath -> IO ()
catFile f = do
    putStrLn ("==> " ++ f)
    handle <- openFile f ReadMode
    catHandle handle

catHandle :: Handle -> IO ()
catHandle h = Monad.forever $ do
    eof <- IO.hIsEOF h
    if eof then do
        hClose h
        exitWith ExitSuccess
    else
        hGetLine h >>= putStrLn

Я запускаю код следующим образом:

runghc cat.hs file1 file2
4b9b3361

Ответ 1

Ваша проблема в том, что exitWith завершает работу всей программы. Таким образом, вы не можете использовать forever для прокрутки файла, потому что, очевидно, вы не хотите запускать функцию "навсегда", вплоть до конца файла. Вы можете переписать catHandle следующим образом

catHandle :: Handle -> IO ()
catHandle h = do
    eof <- IO.hIsEOF h
    if eof then do
        hClose h
     else
        hGetLine h >>= putStrLn
        catHandle h

т.е. если мы не достигли EOF, мы рекурсируем и прочитаем еще одну строку.

Однако весь этот подход слишком сложный. Вы можете написать cat просто как

main = do
    files <- getArgs
    forM_ files $ \filename -> do
        contents <- readFile filename
        putStr contents

Из-за ленивого ввода-вывода все содержимое файла фактически не загружается в память, а передается в стандартный вывод.

Если вам удобно с операторами из Control.Monad, вся программа может быть сокращена до

main = getArgs >>= mapM_ (readFile >=> putStr)

Ответ 2

Если вы установите очень полезный conduit пакет, вы можете сделать это следующим образом:

module Main where

import Control.Monad
import Data.Conduit
import Data.Conduit.Binary
import System.Environment
import System.IO

main :: IO ()
main = do files <- getArgs
          forM_ files $ \filename -> do
            runResourceT $ sourceFile filename $$ sinkHandle stdout

Это похоже на предложенное Шангом простое решение, но используя кабели и ByteString вместо ленивых ввода-вывода и String. Оба из них - хорошие вещи, чтобы научиться избегать: ленивый I/O освобождает ресурсы в непредсказуемые моменты; String имеет много накладных расходов памяти.

Обратите внимание, что ByteString предназначен для представления двоичных данных, а не текста. В этом случае мы рассматриваем файлы как неинтерпретированные последовательности байтов, поэтому ByteString подходит для использования. Если OTOH мы обрабатывали файл как символы для подсчета текста, разбора и т.д., Мы хотели бы использовать Data.Text.

РЕДАКТИРОВАТЬ: Вы также можете записать его так:

main :: IO ()
main = getArgs >>= catFiles

type Filename = String

catFiles :: [Filename] -> IO ()
catFiles files = runResourceT $ mapM_ sourceFile files $$ sinkHandle stdout

В оригинале sourceFile filename создает Source, который читает из именованного файла; и мы используем forM_ снаружи, чтобы перебирать каждый аргумент и запускать вычисление ResourceT по каждому имени файла.

Однако в Conduit вы можете использовать monadic >> для объединения источников; source1 >> source2 - это источник, который производит элементы source1 до тех пор, пока он не будет выполнен, а затем создаст элементы source2. Итак, в этом втором примере mapM_ sourceFile files эквивалентно sourceFile file0 >> ... >> sourceFile filen -a Source, который объединяет все источники.

EDIT 2: И после предложения Дэна Бертона в комментарии к этому ответу:

module Main where

import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString
import Data.Conduit
import Data.Conduit.Binary
import System.Environment
import System.IO

main :: IO ()
main = runResourceT $ sourceArgs $= readFileConduit $$ sinkHandle stdout

-- | A Source that generates the result of getArgs.
sourceArgs :: MonadIO m => Source m String
sourceArgs = do args <- liftIO getArgs
                forM_ args yield

type Filename = String          

-- | A Conduit that takes filenames as input and produces the concatenated 
-- file contents as output.
readFileConduit :: MonadResource m => Conduit Filename m ByteString
readFileConduit = awaitForever sourceFile

На английском языке sourceArgs $= readFileConduit - это источник, который создает содержимое файлов, названных аргументами командной строки.

Ответ 3

catHandle, который косвенно вызывается из catFileArray, вызывает exitWith, когда он достигает конца первого файла. Это завершает работу программы, и дальнейшие файлы больше не читаются.

Вместо этого вы должны просто нормально вернуться из функции catHandle, когда достигнут конец файла. Вероятно, это означает, что вы не должны делать чтение forever.

Ответ 4

Моя первая идея такова:

import System.Environment
import System.IO
import Control.Monad
main = getArgs >>= mapM_ (\name -> readFile name >>= putStr)

Это действительно не сбой в unix-y way и не делает stdin и multibyte, но это "путь больше haskell", поэтому я просто хотел поделиться этим. Надеюсь, что это поможет.

С другой стороны, я предполагаю, что он должен обрабатывать большие файлы легко, не заполняя память, благодаря тому, что putStr уже может удалить строку во время чтения файла.