July 28, 2020
If you're going to write command-line tools, then the first thing you need to know is how to determine the parameters passed in. Shortly after that, you'll need to know how to determine whether a file exists.
Haskell has the function
doesFileExist :: FilePath -> IO Bool
where a FilePath is nothing more than a String:
type FilePath = String
So a program to read command-line arguments and determine whether or not a file exists is easy:
-- This allows access to the arguments (getArgs and getProgName).
import System.Environment
-- This is for doesFileExist
import System.Directory
main = do
args <- getArgs
progName <- getProgName
putStrLn "The arguments are:"
mapM putStrLn args
putStrLn "The program name is:"
putStrLn progName
if null args
then putStrLn "Give me a file name!"
else do
-- Due to 'head', this only checks the first name given.
b <- doesFileExist $ head args
putStrLn $ show b
Now suppose that you want to pass a list of files on the command-line, and check whether they all exist. Lines that have been changed or added are in red.
import System.Environment import System.Directory -- Needed for allM (but see below) import MonadUtils main = do args <- getArgs if null args then putStrLn "Give me some file names!" else do b <- doFilesExist args putStrLn $ show b doFilesExist :: [String] -> IO Bool doFilesExist names = allM (\s -> doesFileExist s) names
To compile the first program, it suffices to use ghc exist.hs (or whatever
you call the source file). For the second program, you may need to use Cabal
to make MonadUtils available. Or you can say
ghc -package ghc exist.hs since MonadUtils is part of the
default installation, but is not visible. Another alternative is to look on Hoogle to
find the source code where the desired function (allM) is defined;
copy it from there into your source code and compile without having to type
-package ghc.
Of course, allM depends on additional functions, and they'll need to be
copied and pasted too.
Another solution – if you don't want to mess with Cabal – is to copy the
necessary function definitions to a new source file. The ghc compiler is
smart enough to detect the dependence and compile the additional source automatically.
In the case above, instead of importing MonadUtils, create a new file
called MyUtils and import that instead.
module MyUtils where -- Copied from MonadUtils. (&&^) :: Monad m => m Bool -> m Bool -> m Bool (&&^) a b = ifM a b (pure False) allM :: Monad m => (a -> m Bool) -> [a] -> m Bool allM p = foldr ((&&^) . p) (pure True) ifM :: Monad m => m Bool -> m a -> m a -> m a ifM b t f = do b <- b; if b then t else f notM :: Functor m => m Bool -> m Bool notM = fmap not
What if you want a program that takes exactly two file names on the command-line?
Adding a test for the number of arguments in main would make it messy.
Instead, define a function:
import System.Environment import System.Directory import MyUtils main = do args <- getArgs -- ifM is a function, so 'then' and 'else' are not explicitly used. ifM (notM $ argsValid args) (putStrLn "Give me the names of two existing files!") (putStrLn "Congratulations. You can type.") argsValid :: [String] -> IO Bool argsValid names = do if (null names) || (length names /= 2) then return False else doFilesExist names doFilesExist :: [String] -> IO Bool doFilesExist names = allM (\s -> doesFileExist s) names
The above feels natural for someone coming from an imperative background, but the
use of ifM and notM seems contrary to the spirit of Haskell.
The user isovector on reddit
suggested replacing
ifM (notM $ argsValid args)
(putStrLn "Give me the names of two existing files!")
(putStrLn "Congratulations. You can type.")
with
argsValid args >>= \case
True -> putStrLn "Congratulations. You can type."
False -> putStrLn "Give me the names of two existing files!"
This feels more Haskellicious, with the data flowing along, kicking out side-effects
as appropriate. It does require the XLambdaCase switch (a "language
extension"), which can be given when ghc is invoked, or you can include the line
{-# LANGUAGE LambdaCase #-}
at the head of the source file.