所以,让我们说我想要计算一个序列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
,但代码已经臭了!
答案 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
序列元素,将i
与n
进行比较 - 如果
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;,这样我们就可以有效地支持每个手指移动;但是因为我知道手指只是单向移动,所以我只是为此进行了优化。