- Even though
module Main
is implied when no header is given, I'd write it.
return . read =<< getLine = read <$> getLine = fmap read getLine
is a worse version of readLn
. read <$> getLine
is basically return (error msg)
when the parse fails. This passes the bottoming value into the "pure" part of your code. readLn badstr
crashes at a well defined time—upon its execution—and if you actually wanted to handle the error you'd use Text.Read.readMaybe
or readEither
.
concat . repeat
is cycle
. Gets your point across better, doesn't it?
putStrLn . unlines
is a bug. It causes an extra newline in the input: unlines
creates a trailing newline and putStrLn
creates another. Use putStr
.
- I don't hate the "expression" style here, but once we replace the
=<< return . read =<< getLine
with =<< readLine
you may as well bring it to the front and use >>=
, and at that point let's just use a do
.
x
? Really?
So:
module Main (main) where -- not my style of spacing; but fine
import System.IO (readFile)
main = do
wanted <- readLn
contents <- readFile "file"
putStr $ unlines $ take wanted $ cycle $ lines contents
Though I'd also take
main = do
wanted <- readLn
putStr . unlines . take wanted . cycle . lines =<< readFile "file"
Or, if you're attached
main = readLn >>= (\wanted -> putStr . unlines . take wanted . cycle . lines =<< readFile "file")
As to your concern about laziness; this is perfectly fine. readFile
is lazy, and so is lines
. cycle
is actually better than concat . repeat
; the latter keeps allocating (:)
-cells as long as take
consumes them, while cycle
will simply connect a pointer that cycles back to the beginning of the output list, which should make it faster. Also, it errors instead of potentially looping forever on an empty input. take
is of course lazy, and we've eliminated the excess laziness that would cause any parse errors to be triggered in it. Passing an infinite input (am on a *nix, so I used yes(1)
and mkfifo(1)
) into this program doesn't break it, which is a good litmus test for laziness.
Now, as to the functionality of this program: the wanted
number should really be an argument, and so should the file (which should be optional and default to stdin). That's done with getArgs
. This is complicated enough to move into new definitions. Let's get that readMaybe
going, too. (Hoogle the new names to find the imports.)
dieWithUsage :: IO a
dieWithUsage = do
name <- getProgName
die $ "Usage: " ++ name ++ " lines [file]"
parseArgs :: [String] -> IO (Int, Handle)
parseArgs [] = dieWithUsage
parseArgs (wanted : rest) = (,) <$> getWanted <*> getHandle
where getWanted = maybe dieWithUsage return $ readMaybe wanted
getHandle = case rest of
[] -> return stdin
[path] -> openFile path ReadMode
_ -> dieWithUsage
leaving
main = do
(wanted, input) <- parseArgs =<< getArgs
putStr . unlines . take wanted . cycle . lines =<< hGetContents input