在 optparse-applicative ReadM 中处理来自 openFile 的异常

handle exception from openFile in a optparse-applicative ReadM

使用 optparse-applicative,我想要一个可选参数,它应该是文件的路径,或者在未指定时,stdin。这里显而易见的选择是使此参数类型为 IO Handle,并且在使用 openFile 传递参数时。这是我目前拥有的:

module Main where

import Data.Semigroup ((<>))
import Options.Applicative
import System.IO

data Args = Args { input :: IO Handle }

parseArgs = Args <$> argument parseReadHandle (value defaultHandle)
  where defaultHandle = return stdin :: IO Handle

parseReadHandle :: ReadM (IO Handle)
parseReadHandle = eitherReader $ \path -> Right $ openFile path ReadMode

getArgs :: IO Args
getArgs = execParser $ info (parseArgs <**> helper) fullDesc

main :: IO ()
main = run =<< getArgs

run :: Args -> IO ()
run (Args input) = putStrLn =<< hGetContents =<< input

问题在于,我们没有正确地 handle 来自 openFile 的异常,而是依赖于未处理异常的默认行为(打印错误并退出)。这看起来很恶心。

我认为更正确的方法是 return Left 和来自 openFile 的错误信息。问题是,eitherReader 期望 String -> Either String a 所以我们不能做这样的事情:

{-# LANGUAGE ScopedTypeVariables #-}
import Control.Exception

parseReadHandle :: ReadM (IO Handle)
parseReadHandle = eitherReader tryOpenFile

tryOpenFile :: IO (Either String (IO Handle)) -> FilePath
tryOpenFile path = do
  handle (\(e :: IOException) -> return $ Left $ show e) $ do
    return $ Right $ openFile path ReadMode

当然,从tryOpenFile的类型可以看出,这个不会进行typecheck。我不确定我要求的是否可行,因为错误消息似乎必须是 IO String,因为要获得错误,必须执行 IO 计算。所以至少看起来你需要 eitherReader 才能获得 String -> IO (Either String a)String -> Either (IO String) (IO Handle)。根据我对它们的基本理解,听起来这里可以使用 monad 转换器来包装 ReadM(或者反过来?)。但这比我的理解要深一些,我不知道如何先行。

有没有办法在 optparse-applicative ReadM 中完成 handleing 一个 IOException

我认为您的方法有些误入歧途。

你说:"I'd like to have an optional argument, which should be a path to a file..."

好的,那么按照 Maybe FilePath 的思路怎么样?这听起来像是你想要的。或等效的 ADT:

data Path = StandardInput | Path FilePath

当你说,"The obvious choice here is to make this argument type IO Handle and when an argument is passed in use openFile" 你超前了。

从命令行进行解析应该是将要解析的输入转换为适合将来在您的程序中使用的数据。不要担心在这个阶段打开文件,或者如果文件不存在时处理异常,或者你将要使用这些数据的任何其他方式...只是担心关于这个问题,我的程序的用户是否给我一个文件路径?也就是说,我有什么数据?其他事情不是(也不应该是)optparse-applicative 的工作。

因此,只需为此数据类型构建您的解析器 Path。它可能由每个构造函数的解析器组成。例如:

stdInputParser :: Parser Path
stdInputParser = ...

pathSuppliedParser :: Parser Path
pathSuppliedParser = ...

pathParser :: Parser Path
pathParser = pathSuppliedParser <|> stdInputParser

无论如何,一旦您 运行 execParser,您将保留 Path 数据类型。所以你将把它作为参数传递给你的 run 函数:

run :: Path -> IO ()
run StandardInput = ... use stdin here
run (Path filePath) = ... use openFile here, catch and handle exceptions if the file doesn't exist, etc.