【问题标题】:Haskell: monadic takeWhile?Haskell: monadic takeWhile?
【发布时间】:2009-07-15 20:27:39
【问题描述】:

我有一些从 Haskell 调用的用 C 语言编写的函数。这些函数返回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]

这给了我这个错误:

:1:22: 无法将预期类型“[b]”与推断类型“IO a”匹配 在 `mapM' 的第一个参数中,即 `f' 在`($)'的第二个参数中,即`mapM f([0 .. 5])' 在表达式中:takeWhile (

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

为了 takeWhile 的目的,我可以从f 中“取消”这些值吗?

这是适合函子的解决方案吗? Functors 还没有和我一起“点击”,但我有一种印象,这可能是使用它们的好时机。


更新:

@sth 有最接近我想要的答案 - 事实上,这几乎正是我想要的,但我仍然想看看是否有一个 standard 解决方案不是' t 显式递归——毕竟这是 Haskell!回顾我如何表达我的问题,现在我可以看到我对自己想要的行为不够清楚。

我上面用作示例的f 函数只是一个示例。真正的函数是用 C 语言编写的,专门用于它们的副作用。我不能使用@Tom 对mapM_ f (takeWhile (&lt;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,但似乎什么都没有发生。

【问题讨论】:

  • sortBy 没有按照sortByM :: Monad m =&gt; (a -&gt; a -&gt; m Ordering) -&gt; [a] -&gt; m [a] 定义也有点失望。

标签: haskell monads chaining


【解决方案1】:

您可以将sequence 定义为

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

你看到的liftM2的问题是你没有机会阻止m2,这可能是launchTheMissiles

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

注意[1..] 的使用,它显示了新的组合子maintains laziness


你可能更喜欢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,sequenceWhile 的定义很好。我首先尝试使用foldM 而不是foldr 来定义它,但这显然行不通,因为这仍然会强制整个列表。
  • 有人建议了一个更好的名字:sequenceUntil。我还添加了 spanM 作为替代方案。
【解决方案2】:

我认为标准库中没有像 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]

【讨论】:

  • 为了与Control.Monad.filterM 保持一致,我希望有一个更像takeWhileM :: (Monad m) =&gt; (a -&gt; m Bool) -&gt; [a] -&gt; m [a] 的类型;这将使用法类似于join . liftM sequence . takeWhileM (liftM (&lt; 4))。但这对于 OP 想要的东西来说有点丑陋,所以嗯。
  • @sth, @ephemient:不在标准库中,但在我新发布的“生成器”包(在 hackage 中)中有一个更通用的 takeWhile 函数。你的 takeWhileM 需要动作列表不依赖于 monad 内的动作,而生成器的 takeWhile 获取一个 monad 为 m 的列表,允许相同甚至更多。
  • +1 用于完全匹配我想要的行为。我更新了问题,我将拭目以待,看看是否有人有标准的 lib 解决方案。如果没有,我接受你的回答。
【解决方案3】:

编辑:现在我知道你在找什么了。

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 -&gt; b) 转换为函数 (m a -&gt; m b),其中 m 是 Monad。

(顺便说一句,您可以通过在Hoogle 上搜索其类型来找到这样的函数,在这种情况下通过搜索:Monad m =&gt; (a -&gt; b) -&gt; (m a -&gt; 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 函数。

如果您想在第三个元素之后停止打印,则必须停止调用 print。这意味着,不要将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)首先打印所有内容,然后然后返回一个列表。

【讨论】:

  • 这不是我想要的真实行为,但 +1 以获得很好的解释。 liftM 现在更有意义了。
【解决方案4】:

您可以使用"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 的单子列表,它不执行单子操作
  • fmap f 到该列表会导致 ListT IO (IO Int) 仍然不执行单子动作,只包含单子​​动作。
  • joinM 将其转换为 ListT IO Int。当物品被消费时,每个包含的动作都会被执行,其结果将是列表中的值。
  • takeWhile 泛化为任何 List[] 和“Monad m =&gt; ListT m”都是List 的实例。
  • execute 使用单子列表,执行其所有操作。
  • 如果您对结果感兴趣,可以使用"toList :: List m =&gt; m a -&gt; ItemM m [a]"(“ItemM (ListT IO)”是IO)。所以在这种情况下它是“toList :: ListT IO a -&gt; IO [a]”。更好的是,您可以继续使用诸如scanl 等高阶函数来处理正在执行的单子列表。

【讨论】:

    【解决方案5】:

    最近,您可以使用包含 handy functionsMonadList hackage,例如 takeWhileM、dropWhileM、deleteByM 等等。

    【讨论】:

      猜你喜欢
      • 2013-06-04
      • 2012-07-05
      • 1970-01-01
      • 2016-12-14
      • 2014-06-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多