哈斯克尔:monadic takeWhile?

时间:2009-07-15 20:27:40

标签: haskell monads chaining

我有一些用C语言编写的函数,我从Haskell调用。这些函数返回IO (CInt)。有时我想运行所有函数,无论它们返回什么,这很容易。为了示例代码,这是当前正在发生的事情的一般概念:

Prelude> let f x = print x >> return x
Prelude> mapM_ f [0..5]
0
1
2
3
4
5
Prelude>

我得到了我想要的副作用,我不关心结果。但是现在我需要在第一个没有返回我想要的结果的项目之后立即停止执行。假设返回值为4或更高要求执行停止 - 那么我想要做的是:

Prelude> takeWhile (<4) $ mapM f [0..5]

这给了我这个错误:

<interactive>:1:22:
    Couldn't match expected type `[b]' against inferred type `IO a'
    In the first argument of `mapM', namely `f'
    In the second argument of `($)', namely `mapM f ([0 .. 5])'
    In the expression: takeWhile (< 4) $ mapM f ([0 .. 5])

这对我来说很有意义 - 结果仍然包含在IO monad中,我不能只比较IO monad中包含的两个值。我知道这正是monad的目的 - 将结果链接在一起并在满足某个条件时丢弃操作 - 但在这种情况下是否有一种简单的方法来“封装”IO monad以在条件下停止执行链我选择的,没有编写MonadPlus的实例?

为了takeWhile的目的,我可以从f“解除”值吗?

这是算子适合的解决方案吗? Functors还没有“点击”我,但我觉得这可能是一个很好的使用方式。

<小时/> 的更新

@sth对我想要的答案最接近 - 事实上,这几乎就是我想要的,但我仍然想知道是否有标准的解决方案显式递归 - 毕竟这是Haskell!回顾我如何措辞我的问题,现在我可以看到我对自己想要的行为不够清楚。

我上面用作示例的f函数仅仅是一个例子。实际功能是用C语言编写的,专门用于副作用。我无法使用@ Tom对mapM_ f (takeWhile (<4) [0..5])的建议,因为我不知道任何输入在执行之前是否真的会导致成功或失败。

我实际上并不关心返回的列表 - 我只想调用C函数,直到列表用完或第一个C函数返回失败代码。

在C风格的伪代码中,我的行为是:

do {
    result = function_with_side_effects(input_list[index++]);
} while (result == success && index < max_index);

再次,@ sth的答案执行我想要的确切行为,除了结果可能(应该?)被丢弃。 dropWhileM_函数对我来说是等价的。为什么Control.Monad中没有这样的函数或takeWhileM_?我看到有a similar discussion on a mailing list,但似乎没有任何结果。

5 个答案:

答案 0 :(得分:15)

您可以将sequence定义为

sequence xs = foldr (liftM2 (:)) (return []) xs

您遇到liftM2的问题是,您没有机会停止m2launchTheMissiles可能会停止!

liftM2 :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
liftM2 f m1 m2 = do
    x1 <- m1
    x2 <- m2
    return (f x1 x2)

如下所示使用guard似乎很有吸引力:

sequenceUntil p xs = foldr (myLiftM2 p (:)) (return []) xs
  where myLiftM2 p f m1 m2 = do
            x1 <- m1
            guard $ p x1
            x2 <- m2
            return (f x1 x2)

上面的代码在您的应用程序中将失败,因为IO monad不是MonadPlus的实例。

所以多握一点手

module Main where

import Control.Monad

printx :: Int -> IO Int
printx x = do
    print x
    return x

sequenceUntil :: (Monad m) => (a -> Bool) -> [m a] -> m [a]
sequenceUntil p xs = foldr (myLiftM2 (:) []) (return []) xs
  where myLiftM2 f z m1 m2 = do
            x1 <- m1
            if p x1 then do x2 <- m2
                            return $ f x1 x2
                    else return z

main :: IO ()
main = do
  let as :: [IO Int]
      as = map printx [1..10]
  ys <- sequenceUntil (< 4) as
  print ys

即使as是1到10之间的操作列表,输出也是

1
2
3
4
[1,2,3]

放弃结果是微不足道的:

sequenceUntil_ :: (Monad m) => (a -> Bool) -> [m a] -> m ()
sequenceUntil_ p xs = sequenceUntil p xs >> return ()

main :: IO ()
main = do
  let as :: [IO Int]
      as = map printx [1..]
  sequenceUntil_ (< 4) as

请注意使用显示新组合子maintains laziness[1..]


您可能更喜欢spanM

spanM :: (Monad m) => (a -> Bool) -> [m a] -> m ([a], [m a])
spanM _ [] = return ([], [])
spanM p (a:as) = do
  x <- a
  if p x then do (xs,bs) <- spanM p as
                 return (x:xs, bs)
         else return ([x], as)

请注意,它与span略有不同,因为它包含结果列表中的失败元素。这对是剩下的动作。例如:

*Main> (xs,bs) <- spanM (< 4) as
1
2
3
4
*Main> xs  
[1,2,3,4]
*Main> sequence bs
5
6
7
8
9
10
[5,6,7,8,9,10]

另一种选择:

untilM :: Monad m => (a -> Bool) -> [m a] -> m ()
untilM p (x:xs) = do
  y <- x
  unless (p y) $ untilM p xs

请注意,谓词的意义是补充的:

*Main> untilM (>= 4) as
1
2
3
4

答案 1 :(得分:14)

我不认为标准库中有takeWhileM之类的内容,但您可以自己编写,以便只执行所需的IO:

takeWhileM :: (Monad m) => (a -> Bool) -> [m a] -> m [a]
takeWhileM _ [] = return []
takeWhileM p (a:as) =
   do v <- a
      if p v
         then do vs <- takeWhileM p as
                 return (v:vs)
         else return []

仅在找到与谓词不匹配的元素之前评估提供的列表:

*Main> takeWhileM (<4) (map f [1..5])
1
2
3
4
[1,2,3]

答案 2 :(得分:10)

编辑:现在我看到了您要找的内容。

gbacon发布了一个不错的sequenceWhile函数,这几乎就是你需要的“原始”函数。

实际上,由于您只对副作用感兴趣,sequenceWhile_应该足够了。这是一个定义(再次受到gbacon的启发,投票给他!):

sequenceWhile_ :: (Monad m) => (a -> Bool) -> [m a] -> m ()
sequenceWhile_ p xs = foldr (\mx my -> mx >>= \x -> when (p x) my)
                            (return ()) xs

你这样称呼:

Prelude Control.Monad> sequenceWhile (<4) $ map f [1..]

原始回答:

你不能只是“解除”IO Monad中与takeWile一起使用的值,但你可以“提升”takeWhile以便在Monad中使用!

liftM函数会将函数(a -> b)带到函数(m a -> m b),其中m是Monad。

(作为旁注,您可以通过在Hoogle上搜索其类型来找到这样的函数,在这种情况下,搜索:Monad m => (a -> b) -> (m a -> m b)

使用liftM,您可以执行此操作:

Prelude> :m + Control.Monad
Prelude Control.Monad> let f x = print x >> return x
Prelude Control.Monad> liftM (takeWhile (<4)) $ mapM f [0..5]
0
1
2
3
4
5
[0,1,2,3]

现在,这可能不是你想要的。在返回列表之前,mapM将按顺序将f函数应用于整个列表。然后将结果列表传递给提升的takeWhile函数。

如果要在第三个元素后停止打印,则必须停止调用打印。这意味着,不要将f应用于此类元素。所以,你最终会得到一些简单的东西:

Prelude> mapM_ f (takeWhile (<4) [0..5])

顺便说一下,你是否想知道为什么 mapM将首先打印所有内容,然后再返回列表。您可以通过将函数替换为其定义来看到这一点:

mapM f [0..1]
=
sequence (map f [0..1])
=
sequence (f 0 : map f [1..1])
=
sequence (f 0 : f 1 : [])
=
sequence ((print 0 >> return 0) : f 1 : [])
= 
sequence ((print 0 >> return 0) : (print 1 >> return 1) : [])
=
do x  <- (print 0 >> return 0)
   xs <- (sequence ((print 1 >> return 1) : []))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y  <- (print 1 >> return 1)
             ys <- sequence ([])
             return (y:ys))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y  <- (print 1 >> return 1)
             ys <- return []
             return (y:ys))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y <- (print 1 >> return 1)
             return (y:[]))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (print 1 >> return (1:[]))
   return (x:xs)
=
do x <- (print 0 >> return 0)
   print 1
   return (x:1:[])
=
do print 0
   print 1
   return (0:1:[])

将函数替换为其定义的过程称为等式推理

如果我没有犯任何错误,你现在可以(希望)看到mapM(使用sequence)首先打印所有内容,然后返回一个列表。

答案 3 :(得分:6)

您可以使用"List"包中的那个。

import Control.Monad.ListT (ListT)
import Data.List.Class (execute, fromList, joinM, takeWhile)
import Prelude hiding (takeWhile)

f x = print x >> return x
main =
  execute . takeWhile (< 4) .
  joinM $ fmap f (fromList [0..5] :: ListT IO Int)
  • fromList [0..5]创建一个包含0..5的monadic列表,该列表不执行任何monadic操作
  • fmap f到该列表会产生一个ListT IO (IO Int),它仍然不执行任何monadic操作,只包含一个。
  • joinM将其转换为ListT IO Int。当项目被消耗时,每个包含的动作都会被执行,其结果将是列表中的值。
  • takeWhile适用于任何List[]和“Monad m => ListT m”都是List
  • 的实例
  • execute使用monadic列表,执行其所有操作。
  • 如果您对结果感兴趣,可以使用"toList :: List m => m a -> ItemM m [a]"(“ItemM (ListT IO)”为IO)。所以在这种情况下,它是“toList :: ListT IO a -> IO [a]”。更好的是,您可以继续使用诸如scanl之类的高阶函数来处理正在执行的monadic列表。

答案 4 :(得分:3)

最近,您可以使用包含MonadListhandy functions hackage,例如takeWhileM,dropWhileM,deleteByM等等。