1

I am trying to run an "infinite" simulation, printing the results of each step.

There's a function nextFrameR which takes an input Map and steps forward the simulation to return an output Map, and then there's a render_ function which takes an input Map and prints some things to stdout, returning the input Map (so that I can use iterate or something like it).

I'm really struggling to put all these pieces together as am relatively new to Haskell. I found this answer extremely interesting but am not sure how to put it directly into practice due to the combination of the two functions (I have tried playing with liftM2 and iterate).

The type signatures are as follows:

nextFrameR :: Map -> IO Map
render_ :: Map -> IO Map -- originally Map -> IO ()

I'm not really sure where to go from here, I can do something like:

(iterate (>>= nextFrameR) initialMap) :: [IO Map]

But that just gives me an (infinite?) list of frames (I think), which is great, it just doesn't let me print them as I don't know how to combine the rendering function in there.

GTF
  • 8,031
  • 5
  • 36
  • 59
  • Expression `sequence (iterate (>>= nextFrameR) initialMap) ` would have the `IO [Map]` type. But, rather than involving IO at an early stage, you could use: `frames = iterate nextFrameR initialMap`, write an ordinary render function of type `Map -> String`. Then use `map render frames` to see the textual trace output. IMHO the decision of whether to use IO or not should be left to the top level calling code. – jpmarinier Apr 04 '21 at 13:17
  • The problem is that `nextFrameR` creates a `newStdGen` on each run. I have a version of the code which takes `RandomGen` (which is what `nextFrameR` calls under the hood) but I'm not at the top level. – GTF Apr 04 '21 at 14:04
  • I see, but it is a bit unusual to create a new generator at each step. You could pass the final state of the random number generator in step N to be the initial state of the generator in step N+1. That way you would have statistical guarantees, which are only available within a single random serie. And if you want to be able to run a later simulation with the same random numbers but different physical parameters, newStdGen makes that impossible, as you cannot control the seed. – jpmarinier Apr 04 '21 at 14:31
  • @jpmarinier that's why I have a version of the function which takes `RandomGen` instead (https://github.com/gfarrell/tron-lines/blob/f63a6af9a4d7483b1ddfeff43c3c16bcc83675e5/src/Simulation.hs#L18-L28). I think that fixes the issue you're talking about but perhaps not? I did want to be able to have reproducible results for testing. – GTF Apr 04 '21 at 16:20
  • not exactly. I see in your Github page that your function has this type signature: `nextFrame :: RandomGen gen => gen -> Map -> Map`. So it does not preserve the final state of the generator for further processing. You might want to have this signature: `nextFrame :: RandomGen gen => gen -> Map -> (Map, gen)`. But I see that the **library** `shuffle'` function *drops* the final state. See the call to `fst` in its source code, and no call to `snd`. So you would have to tweak the `shuffle'` source code or arrange to use `shuffleM` with `runRand` instead. – jpmarinier Apr 04 '21 at 19:57
  • on another topic, you really do not want to have StdGen hardwired in your code. Depending on whether you have version 1.1 or 1.2 of the System.Random package, StdGen is a completely different beast. [Motivations for the changes here](https://alexey.kuleshevi.ch/blog/2021/01/29/random-interface/) – jpmarinier Apr 04 '21 at 20:01
  • @jpmarinier thanks, that's really helpful. I'll look at changing `nextFrameR` and using a different `shuffle` function – GTF Apr 05 '21 at 11:26

1 Answers1

2

iterate works fairly well for non-IO computation, but if you are in IO you can't easily exploit iterate.

To see why, your list

(iterate (>>= nextFrameR) initialMap) :: [IO Map]

is

[ initialMap
, initialMap >>= nextFrameR
, initialMap >>= nextFrameR >>= nextFrameR
...

so... how could we exploit that to have an infinite loop? We can't take the non-existent "last element". We also can't execute all the operations in that list in sequence, since that would run initialMap many times.

It's easier if we avoid using iterate and resort to basics like recursion:

loop :: Map -> IO ()
loop m = do
   m' <- nextFrameR m
   render_ m'       -- it looks like you want this
   -- feel free to add some delay here, or some stopping condition to exit the loop
   loop m'

main :: IO ()
main = do
   m <- initialMap
   loop m

You can turn the above code into some code which uses >>= but there is no need to.

Finally, there's no need to make render_ return the same Map. You can make that return IO ().

If you are a beginner, I'd recommend to initially stay away from "smart" library functions like mapM_, traverse, for, sequence, liftM2, ap, ... and learn to do everything using only do-blocks and recursion. Then, once you get how this works, you can try to improve your code exploiting the library helpers.

chi
  • 111,837
  • 3
  • 133
  • 218
  • My instinct to solve this would have been breaking it out using a `do` block, but having written some code using that and then refactored to use >>= etc I was trying to do the same. You're right that I probably should have tried the easier version first, which I'll try now to see if it works. – GTF Apr 04 '21 at 13:49
  • This worked, but as per the original question, is there a good way to use the various operators to transform the code into something "smarter" or not really? It feels like this is the sort of thing these monads and various operators are made for. – GTF Apr 04 '21 at 14:03
  • 1
    @GTF It's a matter of taste. The simple `do` code is short and readable by anyone. I can't immediately see how to achieve this using only _commonly used_ helpers. Surely there is a plethora of non-commonly used helpers, e.g. [`iterateM`](https://hackage.haskell.org/package/monad-loops-0.4.3/docs/Control-Monad-Loops.html#v:iterateM_) which does exactly what you need, it seems. This should still be OK, but always check if the code is still readable when you use advanced helpers. There's no point in writing "clever" code when it's unreadable. – chi Apr 04 '21 at 14:11
  • 1
    "base" not including a proper streaming API can be confusing to newcomers. One learns those nifty list processing functions that work perfectly in a pure context, but once one adds an effect somewhere one discovers that `mapM` and friends don't "stream" as desired. One is then reduced to writing more "monolithic" monadic blocks which couple generation and processing to a degree, getting the impression that those higher-level functions were reserved for the pure world. – danidiaz Apr 04 '21 at 15:12
  • @chi fair enough, I do agree RE readability. Having come from a very different programming direction to Haskell I just wanted to make sure I was learning the right tools to take advantage of some of its power. – GTF Apr 04 '21 at 16:21
  • 3
    @GTF If you really wanted to use existing operators, you could write `loop = fix ((render_ >=> nextFrameR) >=>)`. There are various other spellings depending on your aesthetics, including `loop = fix (\f -> render_ >=> nextFrameR >=> f)`, `loop = fix (\f n -> render_ n >>= nextFrameR >>= f)`, and `loop = render_ >=> nextFrameR >=> loop`. – Daniel Wagner Apr 04 '21 at 18:46