У меня есть приложение Haskell, которое использует библиотеку optparse-applicative для разбора аргументов CLI. Мой тип данных для аргументов CLI содержит FilePath (как файлы, так и каталоги), Double и т. Д. optparse-applicative может обрабатывать ошибки синтаксического анализа, но я хочу убедиться, что некоторые файлы и некоторые каталоги существуют (или не существуют), числа - >= 0 и т. д.

Что можно сделать, так это реализовать несколько вспомогательных функций, подобных этим:

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 

А потом использую вот так:

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")

Мне это кажется слишком уж спонтанным и некрасивым. Кроме того, мне нужны аналогичные функции почти для каждого написанного мной приложения. Есть ли какие-нибудь идиоматические способы справиться с подобным шаблоном программирования, когда я хочу сделать кучу проверок, прежде чем что-то делать с типом данных? Чем меньше шаблонов задействовано, тем лучше :)

3
Shersh 16 Фев 2018 в 21:51

1 ответ

Лучший ответ

Вместо проверки записи параметров после ее создания, возможно, мы могли бы использовать композиция аппликативного функтора для объединения синтаксического анализа и проверки аргументов:

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

Составной синтаксический анализатор имеет тип Compose (Compose Parser IO) (Errors [String]) Options. Слой IO предназначен для проверки существования файлов, а Errors - это похожий на проверку Applicative от преобразователей , в котором накапливаются сообщения об ошибках. Запуск синтаксического анализатора производит действие IO, которое при запуске производит значение Errors [String] Options.

Код немного многословен, но эти парсеры аргументов можно упаковать в библиотеку и использовать повторно.

Некоторые примеры из ответа:

Λ :main "/tmp" 2
Options {path = "/tmp", count = 2}
Λ :main "/tmpx" 2
["Could not find file."]
Λ :main "/tmpx" 22
["Could not find file.","Incorrect number."]
2
danidiaz 17 Фев 2018 в 18:33