【问题标题】:Reduce nestedness when using successive Either/Maybe使用连续的非此即彼/可能时减少嵌套
【发布时间】:2021-08-09 13:25:50
【问题描述】:

这可能是一个非常基本的 Haskell 问题,但让我们假设以下函数签名

-- helper functions
getWeatherInfo :: Day -> IO (Either WeatherException WeatherInfo)
craftQuery :: WeatherInfo -> Either QueryException ModelQuery
makePrediction :: ModelQuery -> IO (Either ModelException ModelResult)

将以上所有内容链接到一个 predict day 函数中的天真方法可能是:

predict :: Day -> IO (Maybe Prediction)
predict day = do
    weather <- getWeatherInfo day
    pure $ case weather of
        Left ex -> do
            log "could not get weather: " <> msg ex
            Nothing
        Right wi -> do
            let query = craftQuery wi
            case query of
                Left ex -> do
                    log "could not craft query: " <> msg ex
                    Nothing
                Right mq -> do
                    prediction <- makePrediction mq
                    case prediction of
                        Left ex -> do
                            log "could not make prediction: " <> msg ex
                            Nothing
                        Right p ->
                            Just p

在更命令式的语言中,可以执行以下操作:

def getWeatherInfo(day) -> Union[WeatherInfo, WeatherError]:
    pass

def craftQuery(weather) -> Union[ModelQuery, QueryError]:
    pass

def makePrediction(query) -> Union[ModelResult, ModelError]:
    pass

def predict(day) -> Optional[ModelResult]:
    weather = getWeatherInfo(day)
    if isinstance((err := weather), WeatherError):
        log(f"could not get weather: {err.msg}")
        return None

    query = craftQuery weather
    if isinstance((err := query), QueryError):
        log(f"could not craft query: {err.msg}")
        return None

    prediction = makePrediction query
    if isinstance((err := prediction), ModelError):
        log(f"could not make prediction: {err.msg}")
        return None

    return prediction

在许多方面可以说它的类型安全性和笨重程度较低,但也可以说是平淡得多。我可以看到主要区别在于,在 Python 中,我们可以(是否应该是另一回事)使用多个早期的 return 语句来在任何阶段停止流程。但这在 Haskell 中是不可用的(无论如何这看起来很不习惯,并且首先破坏了使用该语言的全部目的)。

然而,当处理一个接一个地链接连续的Either/Maybe 的相同逻辑时,是否有可能在 Haskell 中实现相同的“平坦度”?

-- 按照重复的建议进行编辑:

我可以看到另一个问题是如何相关的,但仅此而已 (相关)——它没有回答这里暴露的问题,即如何 展平 3 级嵌套案例。此外这个问题(这里) 以比另一种更通用的方式暴露问题, 这是非常特定于用例的。我想回答这个问题 (此处)将对社区的其他读者有益, 与其他人相比。

我知道对于经验丰富的 Haskeller 而言,这似乎是多么明显 “just use EitherT”听起来像是一个完全正确的答案,但是 这里的重点是,这个问题是从以下角度提出的 一个不是经验丰富的 Haskeller 的人,也是一个阅读过的人 再次说明 Monad 变压器有其局限性,也许是免费的 monad 或 Polysemy 或其他替代方案是最好的,等等。我猜 这将有助于整个社区拥有这个特定的 在这方面用不同的替代方案回答了这个问题,所以 新手 Haskeller 会发现自己的“迷失在翻译中”稍微少一些 当开始面对更复杂的代码库时。

【问题讨论】:

标签: haskell monads flatten maybe either


【解决方案1】:

要“反向推断” 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'

在从 monad 中提取 variableName' 之前总是将 variableNamelet 绑定有点尴尬。这里实际上是没有必要的(您可以将 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-reduce 去掉。此外,JustNothing 值只是 pureempty 的一个特定示例,您可以通过它们获得以下代码:

{-# 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 monad 的效果。换句话说,您不希望Maybe 成为 monad,而是将其短路属性放在IO monad 之上。因此,您转换 IO monad。您仍然可以将 lift 普通的旧非转换 IO 操作放入 MaybeT 堆栈,并且仍然使用 pureempty 作为可能性,从而获得与没有 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 完成。与登录 IO 相比的优势在于,日志不仅会在某处结束,而且函数的调用者会知道日志已创建,并可以决定是将其放入文件中还是显示它直接在终端上或干脆丢弃它。

但由于您似乎总是只记录 Nothing 案例,因此更好的选择是根本不使用 Maybe 转换器,而是使用 Except 转换器,因为这似乎是您的想法:

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

最后,最后请注意,您实际上并不需要绑定中间变量,因为您总是只需在下一个操作中传递它们。即,您有一个Kleisli arrows 的组合链:

predict = withExcept (("could not get weather: "<>) . msg)
                   . getWeatherInfo
      >=> withExcept (("could not craft query: "<>) . msg)
                   . except . craftQuery
      >=> withExcept (("could not make prediction: "<>) . msg)
                   . makePrediction

【讨论】:

  • 我对这个答案的质量和清晰度感到震惊。谢谢一百万。
  • 感谢您的欣赏! — 我忍不住添加了最后一块实用的珍珠母……
  • Kleisli 构图是有史以来最优雅的东西。我偶尔会在只有一级 monad (IO) 的情况下使用它们,但我不得不承认,你在这里所做的事情更令人印象深刻。
猜你喜欢
  • 2017-08-09
  • 1970-01-01
  • 1970-01-01
  • 2019-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多