Haskell - How to combine two monadic Maybe functions into a single function

Essentially it's using fmap sequenceA . sequenceA . fmap f2.

Using do syntax and breaking it down one step at a time:

result and result' gives you the same result.

data X = X
data Y = Y

f1 :: IO (Maybe [X])
f1 = undefined

f2 :: X -> IO (Maybe Y)
f2 = undefined

result :: IO (Maybe [Y])
result = do
 f1' <- f1
 case f1' of
   Just f1'' -> do
     let a = fmap f2 f1'' :: [IO (Maybe Y)]
     let b = sequenceA a :: IO [Maybe Y]
     fmap sequenceA b :: IO (Maybe [Y])
   Nothing -> pure Nothing

result' :: IO (Maybe [Y])
result' = do
 f1' <- f1
 case f1' of
   Just f1'' -> do
     fmap sequenceA . sequenceA . fmap f2 $ f1''
   Nothing -> pure Nothing

A candidate could be:

result :: IO (Maybe [Y])
result = f1 >>= fmap sequenceA . mapM f2 . concat

Here concat is a function concat :: Foldable f => f [a] -> [a] that will convert a Nothing to an empty list, and a Just xs to xs.

We can then make a mapM f2 to generate an IO [Maybe a], and fmap :: Functor f => (a -> b) -> f a -> f b with sequenceA :: (Applicative f, Traversable t) => t (f a) -> f (t a) to convert an IO [Maybe Y] to an IO (Maybe [Y]).

sequenceA will return a Nothing if the list contains one or more Nothings, and return a Just xs if the list contains only Justs with xs the values that originally have been wrapped in Justs.


note: this answer is not very suitable for Haskell newcomers because it involves a monad transformer.

Once we start mixing IO with the processing and generation of lists, I tend to jump straight away to the Stream monad transformer from streaming, that allows you to cleanly interleave the execution of IO actions with the "yielding" of values to be consumed downstream. In a way, Stream is an "effectful list" that performs effects every time we "extract" a value from it.

Consider this version of f1:

import Streaming
import qualified Streaming.Prelude as S
import Data.Foldable (fold)

f1' :: Stream (Of X) IO ()
f1' = do
    mxs <- lift f1
    S.each (fold mxs)  

lift promotes an IO a action to a Stream (Of x) a that doesn’t yield anything, but returns a as the "final value" of the Stream. (Streams yield zero or more values when consumed, and return a final value of a different type once they are exhausted).

Streaming.Prelude.each takes anything that can be converted to a list and returns a Stream that yields the element of the list. Basically, it promotes pure lists to effectful lists.

And Data.Foldable.fold is working here with the type fold :: Maybe [a] -> [a] to get rid of that Maybe.

Here's the corresponding version of f2:

f2' :: X -> Stream (Of Y) IO ()
f2' x = do
   ys <- lift (f2 x)
   S.each ys

Combining them is quite simple, thanks to Streaming.Prelude.for.

result' :: Stream (Of Y) IO ()
result' = S.for f1' f2'

With functions like Streaming.Prelude.take, we could read one Y from the result without having to perform the effects required by the next Y. (We do need to read all the Xs in one go though, because the f1 we are given already does that).

If we want to get all the Ys, we can do it with Streaming.Prelude.toList_:

result :: IO [Y]
result = S.toList_ result'