Search code examples
randompurescript

Picking random elements from an array or a list


Disclaimer: I'm using PureScript, but also added the Haskell tag because I assume this might behave the same way in both languages and the Haskell community is bigger.

I want to pick a random element from an array, repeatedly. Each time I expect a new, random pick, but the value is always the same on repeated calls. It seems the random function is only evaluated once per running the program.

This always returns the same name on subsequent calls:

import Data.Array (length, unsafeIndex)
import Effect.Random (randomInt)
import Effect.Unsafe (unsafePerformEffect)
import Partial.Unsafe (unsafePartial)

pick :: forall a. Array a -> a
pick arr = unsafePartial $ unsafeIndex arr i where
    i = unsafePerformEffect $ randomInt 0 (length arr - 1)

name :: String
name = pick names

With this workaround, it returns a new random pick each time:

import Data.Array (length, unsafeIndex)
import Effect.Random (randomInt)
import Effect.Unsafe (unsafePerformEffect)
import Partial.Unsafe (unsafePartial)

pick :: forall a. Array a -> a
pick arr = unsafePartial $ unsafeIndex arr i where
    i = unsafePerformEffect $ randomInt 0 (length arr - 1)

-- without the dummy argument, this is not re-evaluated
-- on subsequent calls and always returns the same name
name :: Unit -> String
name _ = pick names

I'm using Data.Array, Effect.Random, Effect.Unsafe and Partial.Unsafe.

I feel like this is an ugly hack. What is the proper way of achieving this?


Solution

  • Thanks to the answer of @amalloy I found what I think is a good solution for my case.

    The key was to keep the Effect from the random number generation (Effect corresponds to IO in Haskell) instead of discarding it with unsafePerformEffect. Effect reflects the fact that some side effect is involved in the computation of that value and it might have different results each time. This is exactly what I want. So with this new type signature it now behaves as I expected: name :: Effect String. Each time the effect is "run", it randomly selects a new string from the array.

    I also use NonEmptyArray now, as @amalloy suggested.

    pick :: forall a. NonEmptyArray a -> Effect a
    pick arr = do
        i <- randomInt 0 (length arr - 1)
        let item = arr !! i
        case item of
            Just one -> pure one
            Nothing -> pure $ head arr
            -- still have to handle the Maybe from (!!) which is
            -- a bit annoying since this obviously can never be Nothing
    
    name :: Effect String
    name = pick names
    
    main :: Effect Unit
    main = do
        name >>= log
        name >>= log
        name >>= log
        -- new pick each time