Picking random elements from an array or a list

Viewed 433

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?

3 Answers

A function which does something different each time you call it is opposed to Haskell's design, and I assume PureScript's as well, based on the name "Effect.Unsafe" you've had to import. It is impossible to write such a function without "cheating" by using something from an Unsafe package, and anyone who interacts with such a function will have a headache.

Instead, give your function a more honest type signature. I don't know the PureScript equivalent, but in Haskell it would be something like this (adapted from Get a random list item in Haskell):

pick :: [a] -> Maybe (IO a)
pick [] = Nothing
pick xs = Just $ do
  i <- randomRIO (0, len)
  pure $ xs !! i
  where len = length xs - 1

First, you acknowledge that if given an empty list, the function cannot actually produce an item from the list1. Then, you acknowledge that this is not a pure function: you must perform IO (maybe PureScript calls this an Effect?) to choose randomly. Now callers are aware both of these effects, and must handle them: by checking for emptiness and by treating this as an IO action and not as a pure value.


1 As Parse, don't validate argues, it would actually be better to have your function accept a NonEmpty a instead of taking [a] and returning a Maybe, but I didn't want to introduce new dependencies here.

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

You might want to attach the “random” tag to your question.

I don't know about PureScript, and documentation seems scarce to the newcomer, but in Haskell circles, it seems to be a rather common complaint: the random number generating function always returning the same value. The usual jokes about random numbers apply.

However, Haskell has an established doctrine regarding random number generation. That does not necessarily involve IO, even though in Haskell the IO monad happens to “host” a random number generator.

In Haskell, you would require:

import  System.Random
import  Control.Monad.Random

The problem is that a function, given the same arguments, always returns the same result.

The solution is that you need to have the initial state of the random number generator included as a function argument, and the new, updated state returned as part of the result. This is what Haskell function randomR :: RandomGen g => (a, a) -> g -> (a, g) does. The first argument is the output range. If your array has 100 elements indexed between 0 and 99, that would be a 2-tuple: (0,99).

Once you have a function returning a single random value, you can easily build a second one returning an arbitrary number of values, like this for example:

randomRn :: (RandomGen g, Random a) => (a, a) -> Int -> g -> ([a], g) 
randomRn range count g0 =
    if (count <= 0)
       then  ([], g0)  -- no values and no change
       else  let (a0, g1) = randomR  range g0
                 (as, gf) = randomRn range (count-1) g1  -- recursive call
             in
               (a0:as, gf)  

You can put your function to use:

main = do
    let  seed          = 4242
         g0            = mkStdGen seed  -- get a generator
         arraySize     = 100::Int
         range         = (0, arraySize-1)
         count         = 20  -- want "count" random indexes into array
         (indexes, gf) = randomRn range count g0

    putStrLn $ "Random indexes v1: " ++ show indexes

Program output:

Random indexes v1: [9,56,13,9,38,86,62,18,77,4,66,65,27,33,68,55,94,15,77,45]

Now, depending on taste, style, problem complexity, you might find the explicit presence of the state bothersome, and want to hide it somehow. For this purpose, Haskell uses a variant of the state monad, known as MonadRandom. Using such an approach, you would use code like this to define a monadic action returning a list of random values:

iterateMn :: MonadRandom mr => (Int, Int) -> Int -> mr [Int]
iterateMn range count =
    if  (count <= 0)  then
        return []  -- no action required
    else
        do
            v1 <- getRandomR range
            vs <- iterateMn range (count-1)
            return (v1:vs)

This is essentially the same code as above, except you don't manage the state explicitly. The action is run that way, using function runRand:

    let action          = iterateMn range count    -- monadic action object
        (indexes2, gf2) = runRand action g0        -- go generate indexes

More details here: SO_q57890878_r11282404

It seems that the PureScript random number generation facility is built atop the Javascript facility. Depending on how stringent your requirements are, it may or may not be good enough. You might decide to bite the bullet and implement, for example, a PureScript version of random number generator MRG32k3A. Its statistical properties are known to be quite strong, and its state has a very small memory size and is thus neatly adapted to functional programming languages. Apparently there are several Lisp implementations already available.

Related