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)
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
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).
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:
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.
MaybeT
monad transformer. – Willem Van OnsemMaybeT
with theMaybeT
constructor: that turns am (Maybe a)
into aMaybeT m a
. – Willem Van OnsemEitherT
: hackage.haskell.org/package/EitherT-0.2.0/docs/… – Willem Van Onsemcase
. In essence, it flattens a '1-level' nestedcase
. There are plenty of other examples here on Stack Overflow that address the issue: stackoverflow.com/q/33005903/126014, stackoverflow.com/q/52016330/126014, stackoverflow.com/q/50136713/126014 – Mark Seemann