如何在链接的IO中间处理错误?

时间:2018-11-20 15:09:11

标签: haskell

假设我有一个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"错误,因此我不需要解析错误消息。

该怎么做?

3 个答案:

答案 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