我正试图在Haskell中实现一个主循环,在C中我会这样写:
EntityInteraction *frame(Entity *eList, EntityInteraction *iList) {
parseInteractions(eList, iList);
return simulateEntities(eList);
}
int main() {
Entity eList[] = {...}
EntityInteraction *iList = NULL;
while(true) {
iList = frame(eList, iList);
}
}
因此,我试图在haskell中通过使框架成为递归函数来复制这一点:
frame :: [Entity] -> [EntityInteraction] -> IO ()
frame eList iList = do
frame (parseInteractions iList eList) (simulateEntities eList)
main :: IO ()
main = do
let entList = [...]
frame entList []
但这只会导致预期的堆栈溢出,所以我的问题是,在haskell中使用可变状态进行主循环的propper方法是什么?
(作为一种爱好,我已经用C编程4年了,我刚刚开始学习haskell)
这是一个有趣的现象,只有在这个空例子中才会出现。
首先,这里有一个最小的例子:
frame :: [a] -> IO ()
frame eList = do
frame (id eList)
main :: IO ()
main = do
frame []
如果使用runghc
运行此操作,则会出现内存不足错误。然而,这两项工作中的任何一项:(如果你用ghc-O2编译它们,你可能会得到输出<<loop>>
,程序终止。不过runghc
没有检测到循环,你可以看到程序在恒定的空间中运行。)
A)
frame :: [a] -> IO ()
frame eList = do
frame eList
main :: IO ()
main = do
frame []
B)
frame :: [a] -> IO ()
frame eList = do
print eList
frame (id eList)
main :: IO ()
main = do
frame []
C)
frame :: [a] -> IO ()
frame eList = do
eList `seq` frame (id eList)
main :: IO ()
main = do
frame []
这是什么原因?好吧,尾递归本身不是问题所在。没有堆栈溢出,而是内存不足错误。为什么,如果您的列表实际上并没有随着每次循环迭代而改变?
是的!函数应用程序本身会构建未求值的thunk,因为您从未使用过这些值!在所有的工作示例中,唯一的区别是实际评估了值,并删除了thunks。
因此,函数调用序列在错误的例子中看起来像这样:
frame []
frame (id [])
frame (id (id []))
frame (id (id (id []))) -- the argument takes more memory with every iteration
...
但在工作示例中是这样的:
frame []
frame []
frame []
frame []
frame []
frame [] -- argument stays the same as 'id' is evaluated away.
尽管这些暴徒对他们自己来说并不太昂贵,但在一个无限的循环中,只要有足够的时间,他们将不可避免地吞噬你所有的记忆。
我想你需要这个:
frame :: [Entity] -> [EntityInteraction] -> IO ()
frame eList iList = do
parseInteractions iList eList
simulateEntities eList
main :: IO ()
main = do
let entList = [...]
forever $ frame entList []
尽管这似乎没有多大意义,例如,elist
总是空列表,因此可以省略。但是,如果您的C解决方案中的parseInteractions
产生/填充eList
,那么可能是
eList <- parseInteractions iList
在这种情况下,问题是parseInteractions
是否真的需要进行IO?