0

I am writing a random walk program in Haskell. The basic idea is generating a series of points, randomly at first, then let these points move randomly to next positions and so on. However, I can't let the function iterate, because it can't remember the previous computed value. How to solve this ?

The following is the code I wrote. The problem is that every time it only starts moving from the positions I gave initially.

import Graphics.Gloss
import System.Random

draw :: IO ()
draw = animate FullScreen white (picture [(1,2),(2,5),(4,7),(3,3)])

picture :: [Point] -> Float -> Picture
picture origin num = pictures [translate x y (circle 10) | (x,y) <- randomNext (round num) origin]

randomNext :: Int -> [Point] -> [Point]
randomNext num origin = zipWith (\(x1,y1) (x2,y2) -> (x1+x2,y1+y2)) r origin
    where r = zip (oner num) (oner (num+1)) 
          oner n = take (length origin) $ randomRs (-5::Float,5) (mkStdGen n)
jpmarinier
  • 4,427
  • 1
  • 10
  • 23
XM Zg
  • 109
  • 6
  • You might want to have a look at a similar question, [SO-q66708304](https://stackoverflow.com/questions/66708304/how-to-chain-binary-functions-in-haskell). Essentially, rather than taking a random seed and returning just values, your `randomNext` function should take a *generator* gen0 as an extra argument, and return the values plus the updated state of the generator, typically as a (values, gen1) pair. – jpmarinier Mar 23 '21 at 13:28
  • @RodrigodeAzevedo - `num` is ultimately passed to `mkStdGen` so it seems to be the random seed. – jpmarinier Mar 23 '21 at 13:36

1 Answers1

2

If we rewrite slightly your randomNext function using more conventional notations and shorter lines, it gives something like this:

import System.Random
type Point = (Float, Float)

nextRandoms1 :: Int -> [Point] -> [Point]
nextRandoms1 seed origins =
    let   add2d = (\(x1,y1) (x2,y2) -> (x1+x2, y1+y2))
          count = length origins
          range = (-5::Float, 5)
          xs    = take count $ randomRs range (mkStdGen (seed+0))
          ys    = take count $ randomRs range (mkStdGen (seed+1))
    in
          zipWith add2d  origins  (zip xs ys)

As you have noted, the function does not return anything to allow for the generation of more random values.

More subtly, it uses 2 distinct random series with adjacent seed values. But the library does not offer any guarantee that these 2 series are uncorrelated. Indeed, some random number generators use their seed as just an offset into a shared very large pseudo-random sequence.

Secondarily, it deals with both generating the position increments and adding them to the initial positions.

To avoid these problems, we could start with a modified function which follows the common convention of taking an initial random generator state, and returning an updated state as part of the result:

randomPointUpdates :: StdGen -> (Float, Float) -> Int -> ([Point], StdGen)
randomPointUpdates gen0 range count =
    if (count <= 0)
      then  ([], gen0)  -- generator unaltered
      else
        let  (dx, gen1)  = randomR range gen0 
             (dy, gen2)  = randomR range gen1
             point       = (dx, dy)
             (rest, gen) = randomPointUpdates gen2 range (count-1)
        in
             (point : rest, gen)

This randomPointUpdates function uses recursion on the number of points. It just generates 2D position increments and does not deal with addition at all.

On top of this function, we can now write another one that does deal with addition. As the range is left hardwired, it takes just two arguments: the initial generator state, and the list of initial point positions:

nextRandoms :: StdGen -> [Point] -> ([Point], StdGen)
nextRandoms gen0 origins =
    let   add2d   = (\(x1,y1) (x2,y2) -> (x1+x2, y1+y2))
          count   = length origins
          range   = (-5::Float, 5)

          (changes, gen1) = randomPointUpdates gen0 range count
          points = zipWith add2d  origins  changes 
    in
          (points, gen1)   

We can test that second function using the ghci interpreter:

 λ> 
 λ> :load q66762139.hs
 Ok, one module loaded.
 λ>
 λ> origins = [(1,2),(2,5),(4,7),(3,3)] :: [Point]
 λ> gen0 = mkStdGen 4243
 λ> 
 λ> fst $ nextRandoms gen0 origins
 [(3.8172607,-0.54611135),(4.0293427,6.095909),(-0.6763873,6.4596577),(3.042204,-1.2375655)]
 λ> 

Next, we can use that to write a function that provides an unlimited supply of updated position sets, again using recursion:

randomPointSets :: StdGen -> [Point] -> [[Point]]
randomPointSets gen0 origins =
    let  (pts1, gen1) = nextRandoms gen0 origins
    in   pts1 : (randomPointSets gen1 pts1) 

Note that the pts1 : code bit in the last line is what “remembers” the previous position set, so to speak.

Instead of recursion, we could also have uses here the unfoldr :: (s -> Maybe (a, s)) -> s -> [a] library function, with s being the state of the generator.

Test program:

printAsLines :: Show α => [α] -> IO ()
printAsLines xs = mapM_  (putStrLn . show)  xs

main = do
    let  seed    = 4243
         gen0    = mkStdGen seed
         origins       = [(1,2),(2,5),(4,7),(3,3)] :: [Point]
         allPointSets  = randomPointSets gen0 origins -- unlimited supply
         somePointSets = take 5 allPointSets
    putStrLn $ show origins
    printAsLines somePointSets

Test program output:

$ q66762139.x
[(1.0,2.0),(2.0,5.0),(4.0,7.0),(3.0,3.0)]
[(3.8172607,-0.54611135),(4.0293427,6.095909),(-0.6763873,6.4596577),(3.042204,-1.2375655)]
[(7.1006527,1.5599048),(8.395166,3.1540604),(-2.486746,9.749242),(2.2286167,-1.868607)]
[(11.424954,-0.13780117),(6.5587683,2.593749),(-2.8453062,7.9606133),(2.1931071,-4.915463)]
[(13.615167,-1.636116),(10.159166,1.8223867),(1.733639,6.011344),(6.2104306,-3.4672318)]
[(16.450119,-2.8003001),(12.556836,5.0577183),(2.8106451e-2,4.4519606),(2.2063198,-0.5508909)]
$ 

Side note: Here, we have used manual chaining of the generator state. For more complex usage of pseudo-random numbers, this technique can become too cumbersome. If so, more powerful monadic notations from the Control.Monad.Random package can be used instead.

jpmarinier
  • 4,427
  • 1
  • 10
  • 23
  • 1
    Note that as of version 1.2, the random package has monadic chaining tools as well. – Carl Mar 24 '21 at 20:43
  • @jpmarinier There is another problem here. I need to write a function for interacting with the animate library. The form of the function is asked to be `Float -> Picture` ( which actually can be wrote as Float -> [Point] ) .That is to produce the next frame of animation. It is passed the time in seconds since the program started . The way I thought is simple : just use `!!` to get the corresponding point. The first second is (randomPointSets gen0 origins) !! 1 , the second is ...!! 2 and so on . However with time goes by , program runs more and more slowly. How to process it? many thanks! – XM Zg Mar 26 '21 at 08:59
  • @XMZg You might want to ask a separate question about this, as it might attract the attention of helpers who are more familiar than me with graphics, and also help other users. Operator !! is expected to be inefficient, because Haskell lists are just linked lists. Instead of indexing, in Haskell you do loops with library functions such as `map`. For example, you have a `draw1 :: α → IO ()` function that does something given a single α value. And you have a list of values `vs :: [α]`. Expression `mapM_ draw1 vs` will give you the single combined action that corresponds to all list elements. – jpmarinier Mar 26 '21 at 11:16