使用连续的“要么/可能"时减少嵌套 [英] Reduce nestedness when using successive Either/Maybe

查看:70
本文介绍了使用连续的“要么/可能"时减少嵌套的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

This is probably a very basic Haskell question, but let's assume the following function signatures

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

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

The naive way of chaining all the above into one predict day function could be:

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

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

In more imperative languages, one could do something like:

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 中我们可以(是否应该是另一回事)使用 make 多个早期的 return 语句来在任何阶段停止流程.但这在 Haskell 中是不可用的(无论如何,这看起来非常不习惯,并且首先会破坏使用该语言的全部目的).

Which is arguably less type-safe and clunkier in many ways, but, also arguably, much flatter. I can see that the main difference is that in Python we can (whether we should is a different story) use make multiple early return statements to stop the flow at any stage. But this is not available in Haskell (and anyway this would look very un-idiomatic and defeat the whole purpose of using the language in the first place).

尽管如此,是否有可能实现相同类型的平坦度"?在 Haskell 中处理相同的逻辑,将连续的 Either/Maybe 一个接一个地链接起来?

Nevertheless, is it possible to achieve the same kind of "flatness" in Haskell when dealing with the same logic of chaining successive Either/Maybe one after the other?

-- 按照重复的建议进行

-- EDIT following the duplicate suggestion:

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

I can see how the other question is related, but it's only that (related) — it doesn't answer the question exposed here which is how to flatten a 3-level nested case. Furthermore this question (here) exposes the problem in a much more generic manner than the other one, which is very use-case-specific. I guess answering this question (here) would be beneficial to other readers from the community, compared to the other one.

我明白对于经验丰富的 Haskellers 来说是多么明显只需使用EitherT";听起来是一个完全有效的答案,但是这里的要点是,这个问题是从一个不是经验丰富的 Haskeller 的人,也是一个读过和再一次,Monad 变压器有其局限性,也许是免费的monad 或 Polysemy 或其他替代品是最好的,等等.我猜这对于整个社区来说是有用的在这方面用不同的替代方案回答了问题,所以新手 Haskeller 会发现自己不那么迷失在翻译中"了.当开始面对更复杂的代码库时.

I understand how obvious it seems to be for seasoned Haskellers that "just use EitherT" sounds like a perfectly valid answer, but the point here is that this question is asked from the perspective of someone who is not a seasoned Haskeller, and also who's read over and again that Monad transformers have their limitations, and maybe Free monad or Polysemy or other alternatives would be best, etc. I guess this would be useful for the community at large to have this specific question answered with different alternatives in that regard, so the newbie Haskeller can find himself slightly less "lost in translation" when starting to be confronted with more complex codebases.

推荐答案

要逆推"monad 转换器是这里的正确工具,请考虑不需要 IO 的情况(例如,因为天气信息来自静态数据库那已经在内存中了):

To "reverse deduce" that monad transformers are the right tool here, consider the situation where no IO is needed (e.g. because the weather information comes from a static database that's already in memory):

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 是一个 monad 的事实来解释如何将其展平:

Just about any Haskell tutorial explains how this can be flattened, using the fact that Maybe is a monad:

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 语句中),但请注意,更常见的情况可能是这种情况:

It's a bit awkward to always bind variableName with let before extracting variableName' from the monad. Here it's actually unnecessary (you can just put getWeatherInfo' day itself in the case statement), but note that it could more generally be this situation:

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 中.

The point being, the stuff you're binding to weather could itself be in the Maybe monad.

避免本质上重复的变量名称的一种方法是使用 lambda-case 扩展名,这允许您 eta-reduce 其中一个.此外,JustNothing 值只是 pureempty,您可以通过它获得以下代码:

One way to avoid the essentially duplicate variable names is to use the lambda-case extension, this allows you to eta-reduce one of them away. Furthermore, the Just and Nothing values are only a specific example of pure and empty, with which you get this code:

{-# 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 的代码几乎相同:

Nice, but you can't work in simply the Maybe monad because you also have effects of the IO monad. In other words, you don't want Maybe to be the monad, but rather place its short-circuiting property on top of the IO monad. Hence you transform the IO monad. You can still lift plain old non-transformed IO action into the MaybeT stack, and still use pure and empty for the maybe-ness, thus getting almost the same code as without 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 相比的优势在于,日志不仅会在 某处 结束,而且您的函数的调用者会知道日志已创建,并且可以决定是将其放入文件中还是显示它直接在终端上或干脆丢弃它.

Finally, you could now go further and also use a transformer layer to handle your logging in a better way. It can be done with WriterT. The advantage over logging in IO is that the log doesn't just end up somewhere, but the caller of your function will know a log is created and can decide whether to put that in a file or show it directly on the terminal or simply discard it.

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

But since you always seem to just log the Nothing cases, a better option is to not use the Maybe transformer at all but the Except one instead, since that seems to be your idea:

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 中,然后它会变得更加简洁:

Indeed, probably your primitives should have been in that monad in the first place, then it gets even more concise:

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 箭头的组合链:

Finally-finally note that you don't really need to bind the intermediate variables, since you always just pass them on the the next action. I.e., you have a composition chain of 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

这篇关于使用连续的“要么/可能"时减少嵌套的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆