3

I have a Haskell application which uses optparse-applicative library for CLI arguments parsing. My data type for CLI arguments contains FilePaths (both files and directories), Doubles and etc. optparse-applicative can handle parse errors but I want to ensure that some files and some directories exist (or don't exist), numbers are >= 0 and etc.

What can be done is an implementation of a bunch of helper functions like these ones:

exitIfM :: IO Bool -> Text -> IO ()
exitIfM predicateM errorMessage = whenM predicateM $ putTextLn errorMessage >> exitFailure 

exitIfNotM :: IO Bool -> Text -> IO ()
exitIfNotM predicateM errorMessage = unlessM predicateM $ putTextLn errorMessage >> exitFailure 

And then I use it like this:

body :: Options -> IO ()
body (Options path1 path2 path3 count) = do
    exitIfNotM (doesFileExist path1) ("File " <> (toText ledgerPath) <> " does not exist") 
    exitIfNotM (doesDirectoryExist path2) ("Directory " <> (toText skKeysPath) <> " does not exist")
    exitIfM (doesFileExist path3) ("File " <> (toText nodeExe) <> " already exist")
    exitIf (count <= 0) ("--counter should be positive")

This looks too ad-hoc and ugly to me. Also, I need similar functionality for almost every application I write. Are there some idiomatic ways to deal with this sort of programming pattern when I want to do a bunch of checks before actually doing something with data type? The less boilerplate involved the better it is :)

Shersh
  • 9,019
  • 3
  • 33
  • 61
  • Using the monad abstraction is the correct way to go here; however, I would instead write a function of type `Options -> Either ErrorMessage ValidatedOptions` (with appropriate definitions for ErrorMessage and ValidatedOptions); maybe you would want `.. -> ErrorT IO .. ..` if you need to check the existance of files (but that is hardly useful; a file which exists now may not exist later - consider reading the contents of the file as part of the validation). – user2407038 Feb 16 '18 at 21:14

1 Answers1

3

Instead of validating the options record after it has been constructed, perhaps we could use applicative functor composition to combine argument parsing and validation:

import Control.Monad
import Data.Functor.Compose
import Control.Lens ((<&>)) -- flipped fmap
import Control.Applicative.Lift (runErrors,failure) -- form transformers
import qualified Options.Applicative as O
import System.Directory -- from directory

data Options = Options { path :: FilePath, count :: Int } deriving Show

main :: IO ()
main = do
    let pathOption = Compose (Compose (O.argument O.str (O.metavar "FILE") <&> \file ->
            do exists <- doesPathExist file
               pure $ if exists
                      then pure file
                      else failure ["Could not find file."]))
        countOption = Compose (Compose (O.argument O.auto (O.metavar "INT") <&> \i ->
            do pure $ if i < 10
                      then pure i
                      else failure ["Incorrect number."]))
        Compose (Compose parsy) = Options <$> pathOption <*> countOption
    io <- O.execParser $ O.info parsy mempty
    errs <- io
    case runErrors errs of
        Left msgs -> print msgs
        Right r -> print r

The composed parser has type Compose (Compose Parser IO) (Errors [String]) Options. The IO layer is for performing file existence checks, while Errors is a validation-like Applicative from transformers that accumulates error messages. Running the parser produces an IO action that, when run, produces an Errors [String] Options value.

The code is a bit verbose but those argument parsers could be packed in a library and reused.

Some examples form the repl:

Λ :main "/tmp" 2
Options {path = "/tmp", count = 2}
Λ :main "/tmpx" 2
["Could not find file."]
Λ :main "/tmpx" 22
["Could not find file.","Incorrect number."]
danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • Looks really neat! I wasn't aware of `Errors` data type. Though it looks like magic at first glance... It's not even obvious to me why `-XApplicativeDo` here is not required and you still can use `do`. – Shersh Feb 16 '18 at 22:31
  • @Shersh The `do exists <- ...` constructs a `IO` action that returns `Errors [String] FilePath`. One can construct the action is a "monadic" way and later compose it applicatively with other actions. The `Validation` Applicative from http://hackage.haskell.org/package/validation is an alternative to `Errors`. https://ro-che.info/articles/2015-05-02-smarter-validation – danidiaz Feb 17 '18 at 00:23
  • As it turns out, this solution is unnecessarily complicated. The maintainer of the library recommends https://www.reddit.com/r/haskell/comments/800ho7/validators_in_optparseapplicative/dutm4i1/ performing validation in the `ReadM` monad, which already provides mechanisms for notifying errors http://hackage.haskell.org/package/optparse-applicative-0.14.2.0/docs/Options-Applicative.html#t:ReadM – danidiaz Feb 26 '18 at 18:34
  • 2
    `ReadM` is a pure monad: `newtype ReadM a = ReadM { unReadM :: ReaderT String (Except ParseError) a }`. I can't check for file existence for example inside this monad. – Shersh Feb 27 '18 at 10:43