使用连续 Either/Maybe 时减少嵌套
Reduce nestedness when using successive Either/Maybe
这可能是一个非常基础的 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 中我们可以(是否应该是另一回事)使用 make multiple early return
语句在任何阶段停止流程。但这在 Haskell 中不可用(无论如何,这看起来非常不合惯用语,并且首先会破坏使用该语言的全部目的)。
然而,当处理一个接一个地链接连续 Either
/Maybe
的相同逻辑时,是否有可能在 Haskell 中实现相同类型的“平坦度”?
-- EDIT following the duplicate suggestion:
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.
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 的情况(例如,因为天气信息来自内存中已经存在的静态数据库):
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 的事实:
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'
之前总是将 variableName
与 let
绑定有点尴尬。这里其实是没有必要的(可以直接把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 去掉其中一个。此外,Just
和 Nothing
值只是 pure
和 empty
的一个具体示例,您可以使用它们获得此代码:
{-# 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
之上单子。因此,您 转换 IO
monad。您仍然可以 lift 普通旧的未转换的 IO 操作进入 MaybeT
堆栈,并且仍然使用 pure
和 empty
作为可能,从而获得几乎相同的代码没有 IO:
predict :: Day -> MaybeT IO Prediction
predict day = do
weather <- <b>liftIO</b> (getWeatherInfo day) >>= \case
Left ex -> empty
Right wi -> pure wi
query <- case craftQuery weather of
Left ex -> empty
Right mq -> pure mq
prediction <- <b>liftIO</b> (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
这可能是一个非常基础的 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 中我们可以(是否应该是另一回事)使用 make multiple early return
语句在任何阶段停止流程。但这在 Haskell 中不可用(无论如何,这看起来非常不合惯用语,并且首先会破坏使用该语言的全部目的)。
然而,当处理一个接一个地链接连续 Either
/Maybe
的相同逻辑时,是否有可能在 Haskell 中实现相同类型的“平坦度”?
-- EDIT following the duplicate suggestion:
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.
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 的情况(例如,因为天气信息来自内存中已经存在的静态数据库):
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 的事实:
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'
之前总是将 variableName
与 let
绑定有点尴尬。这里其实是没有必要的(可以直接把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 去掉其中一个。此外,Just
和 Nothing
值只是 pure
和 empty
的一个具体示例,您可以使用它们获得此代码:
{-# 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
之上单子。因此,您 转换 IO
monad。您仍然可以 lift 普通旧的未转换的 IO 操作进入 MaybeT
堆栈,并且仍然使用 pure
和 empty
作为可能,从而获得几乎相同的代码没有 IO:
predict :: Day -> MaybeT IO Prediction
predict day = do
weather <- <b>liftIO</b> (getWeatherInfo day) >>= \case
Left ex -> empty
Right wi -> pure wi
query <- case craftQuery weather of
Left ex -> empty
Right mq -> pure mq
prediction <- <b>liftIO</b> (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