3
\$\begingroup\$

As a Haskell beginner I'm currently playing around the IO monad and prepared simple program which gets from the user two information:

  • URL (http)
  • Output file path

Then it performs GET request to the provided URL and stores the result in the selected file. HTTP status code of the response is displayed on the standard output

Could you please suggest how such code could be improved? Maybe some places when point-free style can be used? I also feel that the part with confirmation of file overwriting is not very elegant.

import System.IO
import Network.HTTP
import System.Directory
getUrl :: IO String
getUrl = putStr "URL: " >> hFlush stdout >> getLine
confirmOverideOrGetNewPath :: String -> IO String
confirmOverideOrGetNewPath filepath = do
 putStr $ "File " ++ filepath ++ " exists. Override? [y/n] "
 hFlush stdout
 decision <- getLine
 let shouldOverride = (if decision == "y" then True else False)
 case shouldOverride of True -> return filepath
 False -> putStrLn "\nProvide other path" >> getOutputFilepath
getOutputFilepath :: IO String
getOutputFilepath = do
 putStr "Output file path: "
 hFlush stdout
 filepath <- getLine
 file_exists <- doesFileExist filepath
 case file_exists of True -> confirmOverideOrGetNewPath filepath
 False -> return filepath
performGetRequest :: String -> IO (String, Int)
performGetRequest url = do
 rsp <- Network.HTTP.simpleHTTP (getRequest url)
 content <- getResponseBody rsp
 responseCode <- getResponseCode rsp
 return (content, responseCodeToInt responseCode)
 where
 responseCodeToInt :: ResponseCode -> Int
 responseCodeToInt code = let (x,y,z) = code in read (mconcat $ show <$> [x,y,z]) :: Int
saveContentToFile :: FilePath -> String -> IO ()
saveContentToFile = writeFile
printHttpStatusCode :: Int -> IO ()
printHttpStatusCode statusCode = putStrLn ("\nStatus code: " ++ show statusCode)
 >> putStrLn "Content saved into file"
main :: IO ()
main = do
 url <- getUrl
 filepath <- getOutputFilepath
 (content, statusCode) <- performGetRequest url
 saveContentToFile filepath content
 printHttpStatusCode statusCode
asked Apr 2, 2021 at 10:27
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Right from the top I notice you could benefit from some factoring.

prompt :: String -> IO String
prompt str = do
 putStr str
 hFlush stdout
 getLine

Then getURL is prompt "URL: " (and maybe doesn't even need to exist separately!) and confirmOverideOrGetNewPath and getOutputFilepath both lose two lines at the top. You'll notice that I also wrote this function with do-notation, it's a stylistic preference but I just don't like to run monads into one line like that, not sure if I have a great reason why. I guess it just feels right to make imperative-y monadic code look like an imperative language would.

The diversion through shouldOverride in confirmOverideOrGetNewPath is unnecessary, just put your expression in the case-statement.

case decision == "y" of
 True -> ...

But then I'd also skip the check for equality entirely and just pattern match on the returned string.

case decision of
 "y" -> return filepath
 "n" -> getOutputFilepath
 _ -> do
 putStrLn "Unrecognized input"
 confirmOverrideOrGetNewPath filepath

I changed the logic to make the behavior more elucidating. If a user actually enters "n", it's not really necessary to tell them to provide a new path since getOutputFilepath will prompt them for a new one immediately. The user presumably expects that, given their input. If they type something other than "y" or "n", instead of requiring them to input a new path, just check again if they're cool with the override.

In getOutputFilepath all I'd note is that file_exists should be camel cased for consistency.

Over the rest of your program, the only thing I'd switch up would be the whole logic around response codes. In performGetRequest I'd just change the return type to be ResponseCode instead of Int, and push your display logic into printHttpStatusCode (which should probably be printResponseCode). Since ResponseCode is just a type alias for tuples, you can also pattern match in the function call (i.e. in your original version responseCodeToInt (x, y, z) = ... instead of the let). And then it also probably makes more sense to have a pure showResponseCode and leave printing up to the caller.

showResponseCode :: ResponseCode -> String
showResponseCode (i, j, k) = concatMap show [i, j, k]

Then I'd just carry the changes through to main and reorder things for clarity and truth in logic.

main = do
 ...
 (content, statusCode) <- performGetRequest url
 putStrLn $ "Response status: " ++ showResponseCode statusCode
 writeFile filepath content
 putStrLn "Content saved"
answered Apr 2, 2021 at 22:58
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.