class: middle, center # errors --- 1. Load config from Yaml file 2. Validate config and setup services 3. Start application ---- ``` -- user supplied data ProvidedConfig = ... data SmtpConfig = ... data HttpConfig = ... data ConfigError = SmtpConfigError String | HttpConfigError String | ConfigParseError String -- app uses data AppConfig = ... runApp :: AppConfig -> IO () ``` --- ### Load Yaml ``` import Data.Yaml (decodeFileEither, prettyPrintParseException) decodeFileEither :: FromJSON a => FilePath -> IO (Either ParseException a) prettyPrintParseException :: ParseException -> String ``` -- What we have: ``` IO (Either ParseException a) ``` -- What we want: ``` IO (Either ConfigError a) ``` --- ``` loadYaml :: FromJSON a => FilePath -> IO (Either ConfigError a) loadYaml file = do decodeFileEither file >>= pure . \case Left err -> (Left . ConfigParseError . prettyPrintParseException) err Right r -> Right r ``` -- ``` fmap :: (a -> b) -> f a -> f b fmap :: (a -> b) -> Either e a -> Either e b ``` -- What we need: ``` fmapL :: (e -> f) -> Either e a -> Either f a ``` -- ``` import Control.Error (fmapL) loadYaml :: FromJSON a => FilePath -> IO (Either ConfigError a) loadYaml file = decodeFileEither file >>= pure . fmapL (ConfigParseError . prettyPrintParseException) ``` --- ``` import qualified Network testConnection :: IO () testConnection = Network.sendTo "server.example.com" (Network.PortNumber 1234) "" ``` -- ``` > testConnection *** Exception: Network.Socket.getAddrInfo: does not exist... ``` -- ``` import Control.Exception (try) try :: Exception e => IO a -> IO (Either e a) ``` -- ``` tryIO :: IO a -> IO (Either IOException a) tryIO = try ``` -- ``` > tryIO testConnection Left Network.Socket.getAddrInfo: does not exist ... > tryIO testConnection Right () ``` --- ``` validateSmtpConfig :: SmtpConfig -> IO (Either IOException EmailService) validateHttpConfig :: HttpConfig -> IO (Either IOException HttpService) ``` -- After `>>= pure . fmapL (_ . show)`: ``` validateSmtpConfig :: SmtpConfig -> IO (Either ConfigError EmailService) validateHttpConfig :: HttpConfig -> IO (Either ConfigError HttpService) loadYaml :: FilePath -> IO (Either ConfigError ProvidedConfig) ``` --- ``` main = do loadYaml "/path/to/yaml" >>= \case Left e -> putStrLn (show e) Right config -> case validateSmtpConfig (smtpConfig config) of Left e -> putStrLn (show e) Right emailService -> case validateHttpConfig (httpConfig config) of Left e -> putStrLn (show e) Right httpService -> runApp (AppConfig emailService httpService) ``` ### Notice Left & Right expressions #### O christmas tree, o christmas tree... --- ### Ideally ``` someFun :: ??? (Either ConfigError AppConfig) someFun = do config <- loadYaml "/path/to/yaml" emailService <- validateSmtpConfig (smtpConfig config) httpService <- validateHttpConfig (httpConfig config) pure (AppConfig emailService httpService) ``` --- ``` import Control.Error (hoistEither) -- | Upgrade an 'Either' to an 'ExceptT' hoistEither :: Monad m => Either e a -> ExceptT e m a ``` -- ``` type ConfigT = ExceptT ConfigError IO ``` -- ``` -- existing loadYaml :: FilePath -> IO (Either ConfigError ProvidedConfig) loadYaml file = decodeFileEither file >>= pure . fmapL (ConfigParseError . prettyPrintParseException) ``` -- ``` import Control.Monad.IO.Class (MonadIO, liftIO) liftIO :: MonadIO -> IO a -> m a -- instance [safe] MonadIO IO -- instance [safe] MonadIO m => MonadIO (ExceptT e m) -- upgraded loadYaml :: FilePath -> ConfigT ProvidedConfig loadYaml file = liftIO (decodeFileEither file) >>= pure . fmapL (ConfigParseError . prettyPrintParseException) >>= hoistEither ``` --- ``` import Control.Error (fmapLT, fmapRT) fmapLT :: Functor m => (a -> b) -> ExceptT a m r -> ExceptT b m r fmapRT :: Monad m => (a -> b) -> ExceptT l m a -> ExceptT l m b ``` -- ``` -- upgraded again loadYaml :: FilePath -> ConfigT ProvidedConfig loadYaml file = liftIO (decodeFileEither file) >>= hoistEither <&> fmapLT (ConfigParseError . prettyPrintParseException) a <&> b = b <$> a ``` -- ``` -- previous loadYaml :: FilePath -> ConfigT ProvidedConfig loadYaml file = liftIO (decodeFileEither file) >>= pure . fmapL (ConfigParseError . prettyPrintParseException) >>= hoistEither ``` --- ``` loadYaml :: FilePath -> ConfigT ProvidedConfig validateSmtpConfig :: SmtpConfig -> ConfigT EmailService validateHttpConfig :: HttpConfig -> ConfigT HttpService ``` -- ``` load :: ConfigT AppConfig load = do config <- loadYaml "/path/to/yaml" emailService <- validateSmtpConfig (smtpConfig config) httpService <- validateHttpConfig (httpConfig config) pure (AppConfig emailService httpService) ``` -- ``` main = do runExceptT load >>= \case Left e -> putStrLn (show e) Right appConfig -> runApp appConfig ``` --- Which do you prefer? ``` main = do loadYaml "/path/to/yaml" >>= \case Left e -> putStrLn (show e) Right config -> case validateSmtpConfig (smtpConfig config) of Left e -> putStrLn (show e) Right emailService -> case validateHttpConfig (httpConfig config) of Left e -> putStrLn (show e) Right httpService -> runApp (AppConfig emailService httpService) ``` ``` main = do runExceptT load >>= \case Left e -> putStrLn (show e) Right appConfig -> runApp appConfig ``` --- class: middle, center # errors http://hackage.haskell.org/package/errors --- class: middle, center # optparse-generic http://hackage.haskell.org/package/optparse-generic