如何使用高阶函数实现这种基于 IO 的循环?

How to implement this kind of IO-based loop using higher order functions?

我有一些类似下面的代码,它根据从磁盘读取的随机 sample 更新 state

myloop 0 state = return state
myloop n state = do
  sample <- getRandomSampleFromFile
  myloop (n - 1) (process state sample)

如何使用高阶函数以惯用的方式编写它来避免显式函数和递归(最好不要引入大量库)?


澄清:我不能只做一个replicateM n getRandomSampleFromFile,因为我有大量的样本需要处理,首先将所有样本读入内存是不可行的。

来自

myloop 0 state = return state
myloop n state = do
  sample <- getRandomSampleFromFile
  myloop (n - 1) (process state sample)

拆分读取数据/过程数据

xs <- mapM (const getRandomSampleFromFile) [1..n]

现在拿n个样品,简单地折叠

foldl process state xs

你可以使用应用语法

myloop n state = foldl process state <$> mapM (const getRandomSampleFromFile) [1..n]

或 (thk2 @andrás-kovács)

myloop n state = foldl process state <$> replicateM m getRandomSampleFromFile

如果你想打断读取过程(或者在读取过程中处理数据)那么你必须进入monad

myloop n state = foldM acc state [1..n]
    where acc s _ | breakProcess s = return s
                  | otherwise      = process s <$> getRandomSampleFromFile

但是折叠不会停止,你最初的方法(毕竟)看起来更好。

myloop n state | breakProcess state = return state
               | otherwise          = do
                                        x <- getRandomSampleFromFile
                                        myloop (n - 1) (process state x)

无论如何,我鼓励使用 conduit, pipes,...如果您正在寻找一些流处理。

(顺便说一句,注意你的 getRandomSampleFromFile 函数可能有硬编码配置,这不好)

这应该提供一个提示:

> import Control.Monad
> foldM (\n x -> print (n,x) >> return (n+x)) 0 [10,20,30]
(0,10)
(10,20)
(30,30)
60

在你的例子中,n 是一个索引-状态对(或者只有状态,如果计算中不需要索引),x 是手头的样本。

一旦您理解了它的工作原理,foldr 在使用 monad 时会出奇地多才多艺:

myloop n = foldr w return [1..n] where
    w _ k state = do
        sample <- getRandomSampleFromFile
        k (process state sample)

注:

w _ k state = getRandomSampleFromFile >>= k . process state

所以

myloop n = foldr (\ _ k state -> getRandomSampleFromFile >>= k . process state) return [1..n]

之所以有效,是因为 foldr 的定义:

foldr f z [] = z
foldr f z (x:xn) = f x (foldr f z xn)

: 情况下,将递归调用放入一个 thunk 中,然后 tail 调用 f,将那个 thunk 传递给它。你不一定要考虑这一点,因为在许多简单的折叠中 f 无论如何在它的第二个参数中都是严格的(所以递归调用在 f 的主体被输入之前有效地执行),但 foldr 实际上立即将控制权交给 f ,并让它决定何时(如果有的话)执行递归调用。所以几乎任何递归结构都可以重写为 foldr.

我想提供我的解决方案,因为这已经困扰我一段时间了。

我们需要的是具有以下签名的函数:

iteratively :: Monad m => (a -> m a) -> a -> [m a]

并且应该认为 iteratively m i 将单子动作 m 重复应用于先前动作的连续输出 [1]。输出必须是单子动作数组的原因是我们只对第 n 个单子动作感兴趣,它表示具有 n 个连续应用程序的动作。

我得到的实现是这样的:

iteratively step init = iterate (>>= step) (return init)

现在,已使用初始值 init 重复 n 次的操作 m 是 - 因此类似于您的 myloop:

repeatedly :: Monad m => (a -> m a) -> a -> Int -> m a
repeatedly step init n = iteratively step init !! n

[1]:这里的m代表了returns下一个动作的参数的单子动作——你称之为sample。它可以根据 getRandomSampleFromFileprocess 来实现,因此:

process <$> getRandomSampleFromFile