在Haskell中实现"findM"?

Jus*_* L. 6 haskell

我正在寻找一个基本上就像mapM在列表上的函数- 它执行一系列monadic动作,将列表中的每个值作为参数 - 并且每个monadic函数返回m (Maybe b).但是,我希望它在导致函数返回Just值的第一个参数之后停止,之后不再执行,并返回该值.

好吧,只显示类型签名可能更容易:

findM :: (Monad m) => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
Run Code Online (Sandbox Code Playgroud)

其中b是第一个Just值.将Maybe在结果是从findING(在一个空列表等的情况下),并没有任何跟Maybe在一元函数返回.

我似乎无法通过直接应用库函数来实现它.我可以用

findM f xs = fmap (fmap fromJust . find isJust) $ mapM f xs
Run Code Online (Sandbox Code Playgroud)

这将是有效的,但我测试了这一点似乎所有的monadic动作都是在调用之前执行的find,所以我不能在这里依赖懒惰.

ghci> findM (\x -> print x >> return (Just x)) [1,2,3]
1
2
3
-- returning IO (Just 1)
Run Code Online (Sandbox Code Playgroud)

实现此函数的最佳方法是什么,在第一次"正常"返回后不会执行monadic操作?可以做的事情:

ghci> findM (\x -> print x >> return (Just x)) [1,2,3]
1
-- returning IO (Just 1)
Run Code Online (Sandbox Code Playgroud)

甚至,理想情况下,

ghci> findM (\x -> print x >> return (Just x)) [1..]
1
-- returning IO (Just 1)
Run Code Online (Sandbox Code Playgroud)

希望有一个答案不使用显式递归,并且如果可能的话是库函数的组合?或者甚至可能是一个无点的

Pet*_*lák 17

一个简单的无点解决方案是使用MaybeT变压器.每当我们看到m (Maybe a)我们可以将其包装进去MaybeTMonadPlus立即获得所有功能.由于mplusMaybeT不正是我们所需要的-它运行仅当第一个导致第二个给定的动作Nothing- msum不正是我们所需要的:

import Control.Monad
import Control.Monad.Trans.Maybe

findM :: (Monad m) => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
findM f = runMaybeT . msum . map (MaybeT . f)
Run Code Online (Sandbox Code Playgroud)

更新:在这种情况下,我们很幸运,存在一个monad transformer(MaybeT),mplus它只有我们需要的语义.但在一般情况下,可能无法构建这样的变压器.MonadPlus有一些法律必须满足其他monadic操作.然而,一切都没有丢失,因为我们实际上并不需要a MonadPlus,我们所需要的只是一个合适的幺半群.

所以我们假装我们不会(不能)拥有MaybeT.计算一些操作序列的第一个值由Firstmonoid 描述.如果左边的部分有一个值,我们只需要制作一个不会执行正确部分的monadic变体:

newtype FirstM m a = FirstM { getFirstM :: m (Maybe a) }
instance (Monad m) => Monoid (FirstM m a) where
    mempty = FirstM $ return Nothing
    mappend (FirstM x) (FirstM y) = FirstM $ x >>= maybe y (return . Just)
Run Code Online (Sandbox Code Playgroud)

这个幺半群确切地描述了该过程,而没有引用列表或其他结构.现在我们只使用这个monoid折叠列表:

findM' :: (Monad m) => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
findM' f = getFirstM . mconcat . map (FirstM . f)
Run Code Online (Sandbox Code Playgroud)

此外,它允许我们使用以下方法创建更通用(甚至更短)的功能Data.Foldable:

findM'' :: (Monad m, Foldable f)
        => (a -> m (Maybe b)) -> f a -> m (Maybe b)
findM'' f = getFirstM . foldMap (FirstM . f)
Run Code Online (Sandbox Code Playgroud)

  • @Cirdec我更新了答案.在一般情况下,可能无法构建这样的变压器,但我们实际上并不需要.它足够有一个幺半群并折叠它. (2认同)

Dan*_*zer 9

如果你不介意递归,我喜欢Cirdec的答案,但我认为基于折叠的等效答案非常漂亮.

findM f = foldr test (return Nothing) 
    where test x m = do
              curr <- f x 
              case curr of
                  Just _  -> return curr
                  Nothing -> m
Run Code Online (Sandbox Code Playgroud)

如何理解折叠的一个很好的小测试.


Cir*_*dec 6

这应该这样做:

findM _ [] = return Nothing
findM filter (x:xs) =
    do
        match <- filter x
        case match of
             Nothing -> findM filter xs
             _ -> return match
Run Code Online (Sandbox Code Playgroud)

如果你真的想做点免费(添加为编辑)

下面将Alternative使用仿函数在列表中找到一些东西,使用jozefg的答案中的折叠

findA :: (Alternative f) => (a -> f b) -> [a] -> f b
findA = flip foldr empty . ((<|>) .)
Run Code Online (Sandbox Code Playgroud)

我不能做(Monad m) => m . Maybe一个实例Alternative,但我们可以假装有一个现有的功能:

-- Left biased choice
(<||>) :: (Monad m) => m (Maybe a) -> m (Maybe a) -> m (Maybe a)
(<||>) left right = left >>= fromMaybe right . fmap (return . Just)
-- Or its hideous points-free version
(<||>) = flip ((.) . (>>=)) (flip ((.) . ($) . fromMaybe) (fmap (return . Just)))
Run Code Online (Sandbox Code Playgroud)

然后我们可以findM用同样的方式定义findA

findM :: (Monad m) => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
findM = flip foldr (return Nothing) . ((<||>) .)
Run Code Online (Sandbox Code Playgroud)


sha*_*ang 5

这可以用MaybeTmonad变换器很好地表达出来Data.Foldable.

import Data.Foldable (msum)
import Control.Monad.Trans.Maybe (MaybeT(..))

findM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
findM f = runMaybeT . msum . map (MaybeT . f)
Run Code Online (Sandbox Code Playgroud)

如果你改变你的搜索功能来产生一个MaybeT堆栈,那就变得更好了:

findM' :: Monad m => (a -> MaybeT m b) -> [a] -> MaybeT m b
findM' f = msum . map f
Run Code Online (Sandbox Code Playgroud)

或者无点:

findM' = (.) msum . map
Run Code Online (Sandbox Code Playgroud)

原始版本也可以完全无点,但它变得非常难以理解:

findM = (.) runMaybeT . (.) msum . map . (.) MaybeT
Run Code Online (Sandbox Code Playgroud)