计算递归关系" stream"在Haskell有几个KiB价值的回顾?

时间:2015-12-17 22:35:47

标签: haskell

所以,让我们说我想要计算一个序列notfib,在其最后的 9000 值上递归定义,如下所示:

notfib i | i > 9000  = notfib (i - 1500) - notfib (i - 9000) `xor` 75
         | otherwise = {- an IV -} ...

9000是编译时常量;但主要的要求是拥有一个生成器,可以在世界结束前继续产生价值。当然,在恒定的记忆中。

因为很明显我需要以某种方式将最后9000个元素保存在内存中,所以我尝试在可变数组的顶部实现一个环形缓冲区(看起来像是一个优秀的数据结构)。现在我的代码充斥着合格的导入隐藏前奏,笨拙的记录取消/打包,注释掉STUArray,我放弃了失败的范围检查({{1} })因为Not in scope: V.fromList有多少接口。

我还没写单Data.Vector,但代码已经臭了!

可能有些纯度对我有帮助吗?你会如何解决这个问题?

4 个答案:

答案 0 :(得分:4)

notfib :: [Int]
notfib = {- first 9000 values -} ++ zipWith (\a b -> a - b `xor` 75) (drop (9000 - 1500) notfib) notfib

是通常的伎俩。

那就是说,你可能不想继续引用notfib本身,因为它会增长到你计算的最大值。如何解决这个问题取决于你想对序列做什么。

答案 1 :(得分:3)

为什么要关心函数的操作语义?只需将注意力集中在指称语义上,让Haskell的魔力担心性能:

notfibgo _notfib (i :: Integer)
  | i > 9000  = _notfib (i - 1500) - _notfib (i - 9000) `xor` 75
  | otherwise = i 

请注意,此函数与原始函数几乎完全相同,只是以开放递归方式编写(递归调用由函数替换,该函数仅作为函数的参数给出)。现在我们定义两个版本的算法:

import Data.Function.Memoize (memoize)

slow_notfib = let r = notfib_go r in r 
fast_notfib = let r = memoize (notfib_go r) in r 

也许两个版本都不明显,在这种情况下,对于此主题的详细讨论,请参阅here

最后,一个简单的测试功能:

main = do 
 n:m:_ <- getArgs 
 let f = [slow_notfib,fast_notfib]!!read n 
 print $ f (read m)

一些试验结果:

Yuriy@Yuriy-PC ~/haskell
$ time ./test.exe 0 110000
698695701

real    0m3.111s
user    0m0.000s
sys     0m0.000s

Yuriy@Yuriy-PC ~/haskell
$ time ./test.exe 1 110000
698695701

real    0m0.017s
user    0m0.000s
sys     0m0.000s

即使是中等偏小的值也能提升100倍以上!

答案 2 :(得分:1)

让我们概括一下。

对于某些序列{ a_i | i >= 0 },假设我们k基本案例a_0 ... a_{k-1}k - 所有f的{​​{1}}递归关系a_n },n >= k

a_n = f(a_{n-k}, a_{n-k+1}, ..., a_{n-2}, a_{n-1})

在haskell中,我们可以将此序列编写为 corecursive 无限列表:

as = a_0 : 
     a_1 : 
     {- ... : -} 
     a_kMinus1 : 
     zipWithK f (drop 0 as) (drop 1 as) {- ... -} (drop (k-1) as)

zipWithK f (a_nMinusK:as_nMinusK) {- ... -} (a_nMinus2:as_nMinus2) (a_nMinus1:as_nMinus1) = 
  f a_nMinusK {- ... -} a_nMinus2 a_nMinus1 : zipWithK f as_nMinusK as_nMinus2 {- ... -} as_nMinus1
zipWithK f _ _ {- ... -} _ = []

例如,我们可以小跑斐波那契

fibs = 1 :
       1 :
       zipWith2 (+) (drop 0 fibs) (drop 1 fibs)

zipWith2 f (a_nMinus2:as_nMinus2) (a_nMinus1:as_nMinus1) =
  f a_nMinus2 a_nMinus1 : zipWith2 f as_nMinus2 as_nMinus1
zipWith2 f _ _ = []

关于无限列表的好处是它们让我们任意计算 序列的许多元素。通过拉链定义它们很好,因为它只是 由于我们避免,因此序列的每个元素会产生不变的开销 列出'昂贵的(O(n))随机访问。

那就是说,ai iifiiite列表仍然是一个列表辅助工具,而processiig第一个i元素,你的序列可能油耗Oi(i)的开销, 它仍然有一个糟糕的raidom访问权限 - 所以只需要i元素,但是这个序列也有一个O(i)的开销。

如果您想提供第一个n的randon访问的asynptotics elenents,你可以把那些elenents放在Vector中。然后当你 想要随机访问i序列元素,将in进行比较 - 如果 i小于n,只需在向量中查找(即O(1)),否则 回过头来查看序列的其余部分,即O(i - n)。

  import Data.Vector ((!), Vector)
  import qualified Data.Vector as V

  vectorize n as = lookupV n bv cs
    where (bs, cs) = splitAt n as
          bv = V.fromList bs

  lookupV n av _ i | i < n = av ! i
  lookupV n _ as i = as !! (i - n)

答案 3 :(得分:0)

所以你似乎希望这些东西被垃圾收集,你不想要缓存一个巨大的列表。您可能想要的是一组脊柱严格的队列:

-- strict lists, strict queues.
data SL x = Nil | Cons x !(SL x)
data SQ x = SQ !(SL x) !(SL x)

sl_from_list :: [x] -> SL x
sl_from_list = foldr Cons Nil

sl_reverse :: SL x -> SL x
sl_reverse x = go Nil x where
    go acc Nil = acc
    go acc (Cons x rest) = go (Cons x acc) rest

enqueue :: x -> SQ x -> SQ x
enqueue x (SQ front back) = SQ front (Cons x back)

dequeue :: SQ x -> (x, SQ x)
dequeue (SQ Nil back) = case sl_reverse back of Cons x xs -> (x, SQ xs Nil)
dequeue (SQ (Cons x f') b) = (x, SQ f' b)

使用这些随机数生成器看起来更紧凑:

data MyRandGen x = MRG !(SQ x) !(SQ x)

next_rand (MRG a b) = (cv, MRG a'' b'') where
    (av, a') = dequeue a
    (bv, b') = dequeue b
    cv = bv - av `xor` 75
    a'' = enqueue bv a'
    b'' = enqueue cv b'

请注意,这会导致&#34;摊销&#34; O(1)用于生成新随机数的性能:是的,有时它会在内存中创建9000个新单元的昂贵重新分配,但这只发生在对函数的1/9000调用上。这种保证的关键在于您不会尝试并重试从同一个MRG对象生成相同的随机数,因为它可能处于&#34;即将重新分配大量数据的状态! &#34;因此可能会发生坏事。你可以删除这个&#34;最糟糕的情况&#34;如果你把这些事情做得不严格,那么表现就好了:正如Okasaki和其他人所指出的那样,懒惰是必要的,足以将这些摊销的业务转化为持久的恒定时间结构(你基本上做了以上的一些工作在thunk里面反过来。)

至于清楚地思考发生了什么:基本上我们有一个三指列表。&#34;将手指放入列表是指您使用语义存储([x], [x]),您可以在两个不同的方向导航:forwards (xs, y:ys) = (y:xs, ys)backwards (x:xs, ys) = (xs, x:ys):换句话说,两个列表在他们的&#34; head&#34;加入,第一个列表的[]被认为是整个列表的开头,第二个列表的[]被认为是作为整体清单的最后一部分。

这里我们有一个手指进入列表的开头(过去9000个元素,没有&#34;前面&#34;列表),一个手指进入列表中间(过去1500个元素),以及一根手指进入列表的末尾(过去0个元素,没有&#34;返回&#34;列表)。因为我们知道这个队列结构,所以我们知道如何有效地将中指的前端连接到起始手指的末端(只是将所有元素反转到另一个中),如果我们没有那么我们&#39 ; d想要跟踪不同手指中有多少元素,并且仅仅共享一些&#34;,这样我们就可以有效地支持每个手指移动;但是因为我知道手指只是单向移动,所以我只是为此进行了优化。