Haskell - 如何将两个 monadic Maybe 函数组合成一个函数

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

假设我有这两个数据记录,X 和 Y 以及以下两个函数:

f1 :: IO (Maybe [X])
f2 :: X -> IO (Maybe Y)

我需要先调用 f1 然后,对于返回列表的每个元素(存储在 IO (Maybe) 中)调用 f2,这将导致类似于 IO (Maybe [IO (Maybe Y)])。我怎样才能把它们组合成有意义的东西,比如

result :: Maybe (IO [Y])

result :: IO (Maybe [Y])

?

非常感谢您的帮助:-)

候选人可能是:

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

这里 concat 是一个函数 concat :: Foldable f => f [a] -> [a],它会将 Nothing 转换为空列表,将 Just xs 转换为 xs

然后我们可以让 mapM f2 生成 IO [Maybe a],然后 fmap :: Functor f => (a -> b) -> f a -> f b with sequenceA :: (Applicative f, Traversable t) => t (f a) -> f (t a)IO [Maybe Y] 转换为 IO (Maybe [Y])

如果列表包含一个或多个 Nothing

sequenceA 将 return 一个 Nothing,如果 return 一个 Just xs列表仅包含 Justs 和 xs 最初包含在 Justs 中的值。

本质上是在使用fmap sequenceA . sequenceA . fmap f2

使用 do 语法并一次分解它:

resultresult' 给你相同的结果。

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

注意:这个答案不是很适合Haskell新手,因为涉及到monad transformer

一旦我们开始将 IO 与列表的处理和生成混合,我倾向于直接跳到 Stream monad transformer from streaming,它允许您干净地交错 IO 的执行具有 "yielding" 值的操作将在下游消耗。在某种程度上,Stream 是一个 "effectful list",每次我们 "extract" 从中获取一个值时都会执行效果。

考虑这个版本的 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)  

liftIO a 操作提升为 Stream (Of x) a 不会产生任何结果,但 returns a 作为 "final value" 的流。 (Streams 在消耗时产生零个或多个值,并且 return 在它们耗尽后产生不同类型的最终值)。

Streaming.Prelude.each 接受任何可以转换为列表的内容,并且 returns 一个 Stream 产生列表的元素。基本上,它将纯列表提升为有效列表。

Data.Foldable.fold 在这里使用类型 fold :: Maybe [a] -> [a] 来摆脱 Maybe

这里是f2对应的版本:

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

多亏了 Streaming.Prelude.for

将它们组合起来非常简单
result' :: Stream (Of Y) IO ()
result' = S.for f1' f2'

使用像 Streaming.Prelude.take 这样的函数,我们可以从结果中读取一个 Y,而不必执行下一个 Y 所需的效果。 (不过,我们确实需要一次性阅读所有 X,因为给定的 f1 已经完成了)。

如果我们想得到所有的Y,我们可以用Streaming.Prelude.toList_:

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