Tom Tetlaw
Tom Tetlaw

Reputation: 113

Implement main loop in haskell using mutable state

I'm trying to implement a main loop of sorts in Haskell, in C I would write it like this:

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);
    }
}

So I attempted to replicate this in haskell by making frame a recursive function:

frame :: [Entity] -> [EntityInteraction] -> IO ()
frame eList iList = do
    frame (parseInteractions iList eList) (simulateEntities eList)

main :: IO ()
main = do
    let entList = [...]
    frame entList []

But this just results in a stack overflow as expected, so my question is what is the propper way to do a main loop in haskell that uses mutable state?

(I've been programming in C as a hobby for 4 years and I'm just starting to learn haskell)

Upvotes: 0

Views: 416

Answers (2)

Ingo
Ingo

Reputation: 36339

You need this, I think:

frame :: [Entity] -> [EntityInteraction] -> IO ()
frame eList iList = do
    parseInteractions iList eList
    simulateEntities eList

main :: IO ()
main = do
    let entList = [...]
    forever $ frame entList []

Though it seems not to make much sense, for example, elist is always the empty list and so could be omitted. But if parseInteractions in your C solution produces/fills eList, then probably

eList <- parseInteractions iList

In this case, the question is if parseInteractions really needs to do IO?

Upvotes: 0

firefrorefiddle
firefrorefiddle

Reputation: 3805

It's an interesting phenomenon which only hits you in this empty example.

First, here is a minimal example:

frame :: [a] -> IO ()
frame eList = do    
    frame (id eList) 

main :: IO ()
main = do        
    frame [] 

If I run this with runghc, I get an out of memory error. However, either of these works: (if you compile them with ghc -O2, you might actually get the output <<loop>> and the program terminating. runghc doesn't detect the loop though and you can see the program running in constant space.)

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 [] 

What's the reason for this? Well, tail recursion per se is not the problem. There is no stack overflow, but an out of memory error. Why, if your lists are not actually changed with every loop iteration?

Well, they are! The function application itself builds up unevaluated thunks because you are never using the values! In all of the working examples, the only difference is that the values are actually evaluated and the thunks removed.

So the sequence of functions calls looks something like this in the erronous example:

frame []
frame (id [])
frame (id (id []))
frame (id (id (id []))) -- the argument takes more memory with every iteration
...

But like this in the working examples:

frame []
frame []
frame []
frame []
frame []
frame []                -- argument stays the same as 'id' is evaluated away.

Even though the thunks are not too expensive for themselves, in an infinite loop they will inevitable eat up all your memory, given enough time.

Upvotes: 5

Related Questions