Jiv*_*van 8 monads haskell flatten either maybe
这可能是一个非常基本的 Haskell 问题,但让我们假设以下函数签名
-- helper functions
getWeatherInfo :: Day -> IO (Either WeatherException WeatherInfo)
craftQuery :: WeatherInfo -> Either QueryException ModelQuery
makePrediction :: ModelQuery -> IO (Either ModelException ModelResult)
Run Code Online (Sandbox Code Playgroud)
将上述所有内容链接到一个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
Run Code Online (Sandbox Code Playgroud)
在更多命令式语言中,可以执行以下操作:
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
Run Code Online (Sandbox Code Playgroud)
这在很多方面都可以说是不那么类型安全和笨重的,但也可以说,更平坦。我可以看到主要的区别在于,在 Python 中我们可以(是否应该是一个不同的故事)使用 make 多个早期return
语句在任何阶段停止流程。但这在 Haskell 中是不可用的(无论如何,这看起来非常不习惯,并且首先会破坏使用该语言的全部目的)。
然而,在处理连续Either
/Maybe
一个接一个链接的相同逻辑时,是否有可能在 Haskell 中实现相同类型的“平坦度” ?
- 按照重复的建议进行编辑:
我可以看到另一个问题是如何相关的,但这只是(相关)——它没有回答这里公开的问题,即如何展平 3 级嵌套案例。此外,这个问题(这里)以比另一个问题更通用的方式暴露了这个问题,这是非常特定于用例的。与另一个问题相比,我想回答这个问题(此处)对社区中的其他读者有益。
我明白对于经验丰富的 Haskeller 来说,“只使用任何一个”听起来是一个完全有效的答案,这似乎是多么明显,但这里的重点是,这个问题是从一个不是经验丰富的 Haskeller 的人的角度提出的,也是阅读过的人的角度一遍又一遍地认为 Monad 转换器有其局限性,也许 Free monad 或 Polysemy 或其他替代方案是最好的,等等。我想这对整个社区来说是有用的,可以在这方面用不同的替代方案回答这个特定问题,所以刚开始面对更复杂的代码库时,Haskeller 新手会发现自己“迷失在翻译中”的情况会少一些。
lef*_*out 11
要“反向推断”monad 转换器是此处的正确工具,请考虑不需要 IO 的情况(例如,因为天气信息来自已在内存中的静态数据库):
getWeatherInfo' :: Day -> Either WeatherException WeatherInfo
craftQuery :: WeatherInfo -> Either QueryException ModelQuery
makePrediction' :: ModelQuery -> Either ModelException ModelResult
Run Code Online (Sandbox Code Playgroud)
你的例子现在看起来像
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
Run Code Online (Sandbox Code Playgroud)
几乎任何 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'
Run Code Online (Sandbox Code Playgroud)
这是一个有点尴尬总是绑定variableName
与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'
Run Code Online (Sandbox Code Playgroud)
关键是,你绑定的东西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
Run Code Online (Sandbox Code Playgroud)
很好,但是你不能简单地在Maybe
monad 中工作,因为你也有IO
monad 的效果。换句话说,你不想Maybe
来是单子,而是将其短路财产上的顶部IO
单子。因此,您转换了IO
monad。您仍然可以将普通的旧的非转换 IO 操作提升到MaybeT
堆栈中,并且仍然使用pure
和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
Run Code Online (Sandbox Code Playgroud)
最后,您现在可以更进一步,还可以使用转换器层以更好的方式处理日志。可以用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
Run Code Online (Sandbox Code Playgroud)
事实上,可能你的原语一开始就应该在那个 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
Run Code Online (Sandbox Code Playgroud)
最后 - 最后请注意,您实际上并不需要绑定中间变量,因为您始终只是在下一个操作中传递它们。即,您有一个Kleisli 箭头的组合链:
predict = withExcept (("could not get weather: "<>) . msg)
. getWeatherInfo
>=> withExcept (("could not craft query: "<>) . msg)
. except . craftQuery
>=> withExcept (("could not make prediction: "<>) . msg)
. makePrediction
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
138 次 |
最近记录: |