2012-07-13 35 views
12

Tôi đang cố viết một chương trình đơn giản cat trong Haskell. Tôi muốn lấy nhiều tên tập tin làm đối số và viết từng tệp theo thứ tự STDOUT, nhưng chương trình của tôi chỉ in một tệp và thoát.Làm cách nào để triển khai `cat` trong Haskell?

Tôi cần làm gì để làm cho mã của mình in mọi tệp, chứ không phải mã đầu tiên được truyền vào?

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 

Tôi đang chạy các mã như thế này:

runghc cat.hs file1 file2 

Trả lời

16

Vấn đề của bạn là exitWith chấm dứt toàn bộ chương trình. Vì vậy, bạn không thể thực sự sử dụng forever để lặp qua tệp, bởi vì rõ ràng bạn không muốn chạy hàm "mãi mãi", chỉ cho đến khi kết thúc tệp. Bạn có thể viết lại catHandle như thế này

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

I.e. nếu chúng tôi chưa đạt được EOF, chúng tôi sẽ kiểm tra lại và đọc một dòng khác.

Tuy nhiên, toàn bộ cách tiếp cận này quá phức tạp. Bạn có thể viết mèo chỉ đơn giản là

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

Vì lười biếng, toàn bộ nội dung tệp không thực sự được tải vào bộ nhớ, nhưng được chuyển thành thiết bị xuất chuẩn.

Nếu bạn cảm thấy thoải mái với các nhà khai thác từ Control.Monad, toàn bộ chương trình có thể được rút ngắn xuống còn

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

Tôi đã chuyển câu trả lời được chấp nhận cho bạn bởi vì bạn đã sửa lỗi của mình và cũng giải thích luồng IO lười biếng. – Sam

+0

Làm thế nào để bạn phát âm '> =>'? – Sam

+0

"thành phần kleisli". Tôi không biết bất kỳ tên nào ngắn hơn (ngắn hơn) cho nó. – shang

5

catHandle, được gián tiếp gọi từ catFileArray, gọi exitWith khi nó đạt đến cuối của tập tin đầu tiên. Điều này chấm dứt chương trình và các tệp khác không được đọc nữa.

Thay vào đó, bạn chỉ cần trả lại bình thường từ hàm catHandle khi kết thúc tệp. Điều này có thể có nghĩa là bạn không nên đọc số forever.

+0

Ah, hiểu rồi, cảm ơn! – Sam

4

ý tưởng đầu tiên của tôi là thế này:

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

Nó không thực sự thất bại trong unix theo cách, và không làm stdin cũng không phải công cụ multibyte, nhưng nó là "cách thêm haskell" vì vậy tôi chỉ muốn chia sẻ điều đó. Hy vọng nó giúp.

Mặt khác, tôi đoán nó sẽ xử lý các tệp lớn dễ dàng mà không làm đầy bộ nhớ, nhờ thực tế là putStr có thể đã trống chuỗi trong khi đọc tệp.

+0

+1 cho phương pháp thay thế, cảm ơn. – Sam

17

Nếu bạn cài đặt rất hữu ích conduit package, bạn có thể làm theo cách này:

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 

này trông giống như shang của đề nghị giải pháp đơn giản, nhưng sử dụng đường ống và bể ByteString thay vì lười biếng I/O và String. Cả hai đều là những điều tốt để học cách tránh: các I/O lười biếng giải phóng tài nguyên vào những thời điểm khó lường; String có rất nhiều dung lượng bộ nhớ.

Lưu ý rằng ByteString được dùng để đại diện cho dữ liệu nhị phân, chứ không phải văn bản.Trong trường hợp này, chúng tôi chỉ xử lý các tệp dưới dạng các chuỗi byte không giải thích được, do đó, ByteString là tốt để sử dụng. Nếu OTOH chúng tôi đang xử lý tệp dưới dạng văn bản —kết số ký tự, phân tích cú pháp, v.v. — chúng tôi muốn sử dụng Data.Text.

EDIT: Bạn cũng có thể viết nó như thế này:

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

type Filename = String 

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

Trong bản gốc, sourceFile filename tạo ra một Source mà đọc từ tập tin có tên; và chúng tôi sử dụng forM_ ở bên ngoài để lặp qua từng đối số và chạy tính toán ResourceT trên mỗi tên tệp.

Tuy nhiên trong Conduit bạn có thể sử dụng monome đơn giản >> để nối các nguồn; source1 >> source2 là nguồn tạo ra các phần tử của source1 cho đến khi hoàn thành, sau đó tạo ra các thành phần của source2. Vì vậy, trong ví dụ thứ hai này, mapM_ sourceFile files tương đương với sourceFile file0 >> ... >> sourceFile filen —a Source kết hợp tất cả các nguồn.

EDIT 2: Và sau đây đề nghị Dan Burton trong các bình luận cho câu trả lời này:

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 

Trong tiếng Anh, sourceArgs $= readFileConduit là một nguồn sản xuất các nội dung của các tập tin được đặt tên bởi các đối số dòng lệnh.

+3

+1 một minh chứng tuyệt vời cho sự đơn giản và thanh lịch mà 'ống dẫn' đã đạt được. Tôi tự hỏi liệu một nguồn 'getArgs'-esque Source có được sử dụng hay không. Sau đó, bạn có thể viết 'runResourceT $ sourceArgs $ = readFileConduit $$ sinkHandle stdout' trong đó' sourceArgs :: MonadIO m => Nguồn m String' và 'readFileConduit :: MonadResource m => Conduit Tên tệp m ByteString' –

+0

@DanBurton: Tôi vẫn đang học những ống dẫn, nên tôi quyết định thử sức với nó - và đã thành công trong vòng 10 phút. Tôi sẽ chỉnh sửa phản hồi để thêm phiên bản đó. –

+0

Đây không phải là câu trả lời cho câu hỏi của tôi về mặt kỹ thuật, nhưng nó rất mang tính thông tin mà có thể coi đó là "yêu cầu đọc" cho bất kỳ ai có câu hỏi tương tự về Haskell. – Sam

Các vấn đề liên quan