考虑this excellent blog post中的以下缩写代码:
import System.Random (Random, randomRIO)
newtype Stream m a = Stream { runStream :: m (Maybe (NonEmptyStream m a)) }
type NonEmptyStream m a = (a, Stream m a)
empty :: (Monad m) => Stream m a
empty = Stream $ return Nothing
cons :: (Monad m) => a -> Stream m a -> Stream m a
cons a s = Stream $ return (Just (a, s))
fromList :: (Monad m) => [a] -> NonEmptyStream m a
fromList (x:xs) = (x, foldr cons empty xs)
到目前为止还不错 - 一个monadic,递归数据结构和从列表构建一个的方法。
现在考虑使用常量内存从流中选择(统一)随机元素的函数:
select :: NonEmptyStream IO a -> IO a
select (a, s) = select' (return a) 1 s where
select' :: IO a -> Int -> Stream IO a -> IO a
select' a n s = do
next <- runStream s
case next of
Nothing -> a
Just (a', s') -> select' someA (n + 1) s' where
someA = do i <- randomRIO (0, n)
case i of 0 -> return a'
_ -> a
我没有抓住过去四行中发生的神秘的无限循环井;结果a'
取决于someA
上的递归,其本身可能依赖于a'
,但不一定。
我感受到递归工作者以某种方式累积的氛围。 IO a
累加器中的潜在值,但我显然不能很好地推理它。
有没有人可以解释这个函数如何产生它的行为?
答案 0 :(得分:5)
该代码实际上并不在恒定空间中运行,因为它组成了一个越来越大的IO a
动作,它会延迟所有随机选择,直到它到达流的末尾。只有当我们到达Nothing -> a
案例时,a
中的操作才真正开始运行。
例如,尝试在由此函数生成的无限,恒定空间流上运行它:
repeat' :: a -> NonEmptyStream IO a
repeat' x = let xs = (x, Stream $ return (Just xs)) in xs
显然,在这个流上运行select
不会终止,但你应该看到内存使用率上升,因为它为延迟的动作分配了很多thunk。
这是一个稍微重写的代码版本,随着它的进行做出选择,因此它在恒定的空间中运行,并且应该也更加清晰。请注意,我已将IO a
参数替换为普通a
,这清楚地表明此处没有构建延迟操作。
select :: NonEmptyStream IO a -> IO a
select (x, xs) = select' x 1 xs where
select' :: a -> Int -> Stream IO a -> IO a
select' current n xs = do
next <- runStream xs
case next of
Nothing -> return current
Just (x, xs') -> do
i <- randomRIO (0, n) -- (1)
case i of
0 -> select' x (n+1) xs' -- (2)
_ -> select' current (n+1) xs' -- (3)
顾名思义,current
存储每一步的当前选定值。一旦我们从流中提取下一个项目,我们(1)选择一个随机数并使用它来决定是否(2)用新项替换我们的选择或(3)保持我们当前的选择然后再其余的流。
答案 1 :(得分:2)
这里似乎没有任何“循环”。特别是,a'
不依赖于someA
。 a'
由next
的结果上的模式加工限制。它由someA
使用,而select'
又在右侧使用,但这并不构成一个循环。
IO a
做的是遍历流。它维持着两个积累的论点。第一个是来自流的随机元素(它尚未被选中并且仍然是随机的,因此Int
)。第二个是流中的位置(Nothing
)。
维持的不变量是第一个累加器从我们到目前为止看到的流中均匀地选择一个元素,并且整数表示到目前为止遇到的元素数。
现在,如果我们到达流的末尾(Just
),我们可以返回当前的随机元素,它就可以了。
如果我们看到另一个元素(select'
个案例),那么我们再次调用n + 1
来递归。将元素数量更新为someA
是微不足道的。但是我们如何更新随机元素a
?好吧,旧的随机元素n
以相同的概率在流的前a'
个位置之间进行选择。如果我们选择概率为1 / (n + 1)
的新元素{{1}}并在所有其他情况下使用旧元素,那么我们将在整个流中均匀分布到此时为止。