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 Nothing
s, and return a Just xs
if the list contains only Just
s with xs
the values that originally have been wrapped in Just
s.
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. (Stream
s 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 X
s in one go though, because the f1
we are given already does that).
If we want to get all the Y
s, we can do it with Streaming.Prelude.toList_
:
result :: IO [Y]
result = S.toList_ result'