为了“反向推断”monad 转换器是正确的工具,请考虑不需要 IO 的情况(例如,因为天气信息来自内存中已有的静态数据库):
getWeatherInfo' :: Day -> Either WeatherException WeatherInfo
craftQuery :: WeatherInfo -> Either QueryException ModelQuery
makePrediction' :: ModelQuery -> Either ModelException ModelResult
你的例子现在看起来像
predict' :: Day -> Maybe Prediction
predict' day =
let weather = getWeatherInfo' day
in case weather of
Left ex ->
Nothing
Right wi -> do
let query = craftQuery wi
in case query of
Left ex ->
Nothing
Right mq ->
let prediction = makePrediction' mq
in case prediction of
Left ex ->
Nothing
Right p ->
Just p
几乎任何 Haskell 教程都解释了如何将其扁平化,使用以下事实:Maybe
是一个单子:
predict' :: Day -> Maybe Prediction
predict' day = do
let weather = getWeatherInfo' day
weather' <- case weather of
Left ex -> Nothing
Right wi -> Just wi
let query = craftQuery weather'
query' <- case query of
Left ex -> Nothing
Right mq -> Just mq
let prediction = makePrediction' query'
prediction' <- case prediction of
Left ex -> Nothing
Right p -> Just p
return prediction'
总是绑定有点尴尬variableName
with let
提取之前variableName'
来自单子。这里实际上是不必要的(你可以直接输入getWeatherInfo' day
本身在case
声明),但请注意,更普遍的是这种情况:
predict' :: Day -> Maybe Prediction
predict' day = do
weather <- pure (getWeatherInfo' day)
weather' <- case weather of
Left ex -> Nothing
Right wi -> Just wi
query <- pure (craftQuery weather')
query' <- case query of
Left ex -> Nothing
Right mq -> Just mq
prediction <- pure (makePrediction' query')
prediction' <- case prediction of
Left ex -> Nothing
Right p -> Just p
return prediction'
重点是,你要绑定的东西weather
本身可以在Maybe
monad.
避免本质上重复的变量名称的一种方法是使用 lambda-case 扩展,这允许您将其中之一进行 eta 缩减。此外,Just
and Nothing
值只是一个具体示例pure
and empty https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Applicative.html#t:Alternative,用它你可以得到这个代码:
{-# LANGUAGE LambdaCase #-}
import Control.Applicative
predict' :: Day -> Maybe Prediction
predict' day = do
weather <- pure (getWeatherInfo' day) >>= \case
Left ex -> empty
Right wi -> pure wi
query <- case craftQuery weather of
Left ex -> empty
Right mq -> pure mq
prediction <- pure (makePrediction' query) >>= \case
Left ex -> empty
Right p -> pure p
return prediction
很好,但你不能在里面工作只是Maybe
monad因为你也有以下的影响IO
单子。换句话说,你不想要Maybe
to bemonad,而是将其短路属性放在IO
单子。因此你转换 the IO
单子。你仍然可以lift https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Monad-IO-Class.html#t:MonadIO普通的旧式未转换 IO 操作为MaybeT
堆栈,并且仍然使用pure
and empty
出于可能的考虑,从而获得与没有 IO 时几乎相同的代码:
predict :: Day -> MaybeT IO Prediction
predict day = do
weather <- liftIO (getWeatherInfo day) >>= \case
Left ex -> empty
Right wi -> pure wi
query <- case craftQuery weather of
Left ex -> empty
Right mq -> pure mq
prediction <- liftIO (makePrediction query) >>= \case
Left ex -> empty
Right p -> pure p
return prediction
最后,您现在可以更进一步,还使用转换器层以更好的方式处理日志记录。可以用以下方法完成WriterT https://hackage.haskell.org/package/transformers-0.5.6.2/docs/Control-Monad-Trans-Writer-Lazy.html。相对于IO日志的优点是日志不会直接结束某处,但是函数的调用者将知道创建了日志,并可以决定是否将其放入文件中或直接在终端上显示或直接丢弃它。
但因为你似乎总是只是记录Nothing
情况下,更好的选择是不使用Maybe
变压器除了Except https://hackage.haskell.org/package/transformers-0.5.6.2/docs/Control-Monad-Trans-Except.html相反,因为这似乎是你的想法:
import Control.Monad.Trans.Except
predict :: Day -> ExceptT String IO Prediction
predict day = do
weather <- liftIO (getWeatherInfo day) >>= \case
Left ex -> throwE $ "could not get weather: " <> msg ex
Right wi -> pure wi
query <- case craftQuery weather of
Left ex -> throwE $ "could not craft query: " <> msg ex
Right mq -> pure mq
prediction <- liftIO (makePrediction query) >>= \case
Left ex -> throwE $ "could not make prediction: " <> msg ex
Right p -> pure p
return prediction
事实上,你的原语可能首先应该在那个 monad 中,然后它会变得更加简洁:
getWeatherInfo :: Day -> ExceptT WeatherException IO WeatherInfo
makePrediction :: ModelQuery -> ExceptT ModelException IO WeatherInfo
predict day = do
weather <- withExcept (("could not get weather: "<>) . msg)
$ getWeatherInfo day
query <- withExcept (("could not craft query: "<>) . msg)
$ except (craftQuery weather)
prediction <- withExcept (("could not make prediction: "<>) . msg)
$ makePrediction query
return prediction
最后,最后请注意,您实际上并不需要绑定中间变量,因为您始终只是在下一个操作中传递它们。也就是说,你有一个组合链克莱斯利箭 https://en.wikipedia.org/wiki/Kleisli_category:
predict = withExcept (("could not get weather: "<>) . msg)
. getWeatherInfo
>=> withExcept (("could not craft query: "<>) . msg)
. except . craftQuery
>=> withExcept (("could not make prediction: "<>) . msg)
. makePrediction