If you install the very helpful conduit package, you can do it this way:
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
This looks similar to shang's suggested simple solution, but using conduits and ByteString instead of lazy I/O and String. Both of those are good things to learn to avoid: lazy I/O frees resources at unpredictable times; String has a lot of memory overhead.
Note that ByteString is intended to represent binary data, not text. In this case we're just treating the files as uninterpreted sequences of bytes, so ByteString is fine to use. If OTOH we were processing the file as text—counting characters, parsing, etc—we'd want to use Data.Text.
EDIT: You can also write it like this:
main :: IO ()
main = getArgs >>= catFiles
type Filename = String
catFiles :: [Filename] -> IO ()
catFiles files = runResourceT $ mapM_ sourceFile files $$ sinkHandle stdout
In the original, sourceFile filename creates a Source that reads from the named file; and we use forM_ on the outside to loop over each argument and run the ResourceT computation over each filename.
However in Conduit you can use monadic >> to concatenate sources; source1 >> source2 is a source that produces the elements of source1 until it's done, then produces those of source2. So in this second example, mapM_ sourceFile files is equivalent to sourceFile file0 >> ... >> sourceFile filen—a Source that concatenates all of the sources.
EDIT 2: And following Dan Burton's suggestion in the comment to this answer:
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
In English, sourceArgs $= readFileConduit is a source that produces the contents of the files named by the command line arguments.