假设我有一个readEnv
函数,该函数读取两个环境变量并返回Either值,并且类型为ReadError
作为Left值:
module Main where
import Control.Exception (SomeException(..), handle, throw)
import Data.Typeable (typeOf)
import System.Environment (getEnv)
data ReadError
= MissingHost
| MissingPort
deriving (Show)
main :: IO ()
main = do
eitherEnvs <- readEnv'
case eitherEnvs of
Left err -> print err
Right (port, host) -> print (port, host)
readEnv :: IO (Either ReadError (String, String))
readEnv = do
port <- getEnv "HOST"
host <- getEnv "PORT"
return $ Right (port, host)
readEnv' :: IO (Either ReadError (String, String))
readEnv' = handle missingEnv readEnv
missingEnv :: SomeException -> IO (Either ReadError (String, String))
missingEnv (SomeException e)
| isMissingHost e = do
print e
return $ Left $ MissingHost
| isMissingPort e = do
print e
return $ Left $ MissingPort
| otherwise = throw e
where
isMissingHost e = take 4 (show e) == "HOST"
isMissingPort e = take 4 (show e) == "PORT"
由于我知道getEnv
是一个IO,如果缺少env var,它将抛出该错误(我知道有lookupEnv,但是我的问题是关于如何处理错误,而不是如何避免错误),所以我做了readEnv'
函数,它将捕获IO异常并将其转换为ReadError
类型。
上面的代码有效,但是,我不喜欢这种模式/样式来处理异常,因为为了处理getEnv "HOST"
中的异常,我必须将处理程序置于整个{{1 }},并分析错误消息以区分错误是readEnv
还是MissingHost
。如果错误消息中不包含“ HOST”或“ PORT”,则MissingPort
无法区分哪个missingEnv
调用是异常。
理想情况下,有一种方法可以处理发生异常的地方,并以Left值返回短路。由于我知道getEnv
中唯一的IOException是getEnv "HOST"
错误,因此我不需要解析错误消息。
该怎么做?
答案 0 :(得分:2)
考虑使用ExceptT monad并将正确的抽象添加到getEnv
。
例如:
首先让我们通过样板:
module Main where
import Control.Exception (SomeException(..), handle, throw)
-- N.B. Should use Control.Exception.Safe
import qualified Control.Exception as X
import Data.Typeable (typeOf)
import qualified System.Environment as Env
import Control.Monad.Trans.Except
我们想定义类似IO的内容,但专门用于以一种更可组合的方式处理异常,并且至少允许getEnv
。单子是ExceptT IO:
type MyIO a = ExceptT ReadError IO a
runMyIO :: MyIO a -> IO (Either ReadError a)
runMyIO = runExceptT
应该取消我们在monad中可以执行的操作-请记住,如果您的其余代码多次键入lift
,那么您可能没有正确抽象monad。
getEnv :: String -> MyIO String
getEnv s = ExceptT ((Right <$> Env.getEnv s) `X.catch` hdl)
where hdl :: X.SomeException -> IO (Either ReadError String)
hdl _ = pure $ Left (Missing s)
现在我们可以在main中使用此版本的getEnv
:
main :: IO ()
main = do
eitherEnvs <- runMyIO ( (,) <$> getEnv "HOST" <*> getEnv "PORT" )
case eitherEnvs of
Left err -> print err
Right (port, host) -> print (port, host)
是的,我们确实重新定义了错误类型:
data ReadError
= Missing String
-- N.B an enum strategy such as MissingPort is doable but often has a
-- data-dependency at the call site such as @getEnv "host" MissingHost@
--
-- That would be a lot like your 'missingEnv' function which forms a mapping
-- from user strings to the ADT enum 'ReadError'.
deriving (Show)
答案 1 :(得分:1)
使用lookupEnv
确实还不错。以下提供了与Thomas M. DuBuisson的答案相同的错误处理位置。
module Main where
import System.Environment (lookupEnv)
data ReadError = MissingHost | MissingPort deriving (Show)
type EnvReadResult a = IO (Either ReadError a)
main :: IO ()
main = readEnv >>= either print print
parseEnv :: String -> ReadError -> EnvReadResult String
parseEnv name err = lookupEnv name >>= return . maybe (Left err) Right
readEnv :: EnvReadResult (String, String)
readEnv = do
host <- parseEnv "HOST" MissingHost -- host :: Either ReadError String
port <- parseEnv "PORT" MissingPort -- port :: ditto
return $ (,) <$> host <*> port -- Combine and lift back into IO
parseEnv
接受变量名称和错误以报告变量是否未定义,并返回由IO
包裹的Either
值。 maybe
函数充当“异常处理程序”,将Just
的值重新封装为Right
或将Nothing
转换为适当的Left
值。
Applicative
的{{1}}实例有效地返回找到的第一个错误,或将所有Either
的值组合为一个Right
的值。例如:
Right
您还可以利用应用函子组成的事实。
(,) <$> Right "example.com" <*> Right "23456" == Right ("example.com", "23456")
(,) <$> Left MissingHost <*> Right "23456" == Left MissingHost
(,) <$> Right "example.com" <*> Left MissingPort == Left MissingPort
尤其是,由于readEnv = let host = parseEnv "HOST" MissingHost
port = parseEnv "PORT" MissingPort
in getCompose $ (,) <$> Compose host <*> Compose port
和IO
都是可应用的仿函数,因此Either ReadError
也是如此。
答案 2 :(得分:0)
有了以上两个答案,我想出了一个解决方案
1)不需要额外的复杂性(ExceptT)
2)无需分析错误消息即可区分哪个操作失败。
3)readEnv保持不变。
module Main where
import Control.Exception (IOException, handle)
import System.Environment (getEnv)
data ReadError
= MissingHost
| MissingPort
deriving (Show)
main :: IO ()
main = do
eitherEnvs <- readEnv
either print print eitherEnvs
getEnv' :: String -> ReadError -> IO (Either ReadError String)
getEnv' env err = handle (missingEnv err) $ Right <$> (getEnv env)
readEnv :: IO (Either ReadError (String, String))
readEnv = do
eitherHost <- getEnv' "HOST" MissingHost
eitherPort <- getEnv' "PORT" MissingPort
return $ (,) <$> eitherHost <*> eitherPort
missingEnv :: ReadError -> IOException -> IO (Either ReadError String)
missingEnv err _ = return $ Left err