References to $RANDOM in subshells all returning identical values
Asked Answered
D

3

6

The following short script prints a random ten-digit binary number:

#!/usr/bin/zsh
point=''
for i in `seq 1 10`
do
    echo $RANDOM >/dev/null
    point=$point`if [ $RANDOM -gt 16383 ]; then echo 0; else echo 1; fi`
done
echo $point

However, if I remove the apparently useless echo $RANDOM >/dev/null line, the script always prints either 1111111111 or 0000000000.

Why?

Dye answered 15/9, 2015 at 3:14 Comment(3)
The title of the question is a bit funny now! If I had known to title the question so specifically as that, I probably wouldn't have needed to ask a question...Dye
Admittedly so. The question, though -- if you'd seen this title when you were searching for duplicates before asking (you did search for duplicates, right?), would you have recognized it as your problem? (If not, perhaps the "in subshells" needs to be backed out).Bevel
@CharlesDuffy I did search for duplicates, and found a few that definitely weren't related. I would have at least clicked on this title if I'd seen it in the results. =)Dye
I
8

From the man page:

The values of RANDOM form an intentionally-repeatable pseudo-random sequence; subshells that reference RANDOM will result in identical pseudo-random values unless the value of RANDOM is referenced or seeded in the parent shell in between subshell invocations.

The "useless" call to echo provides the reference that allows the subshell induced by the command substitution to produce a different value each time.

Impulsion answered 15/9, 2015 at 4:4 Comment(0)
B
9

Subshells (as created by backticks, or their modern replacement $()) execute in a different context from the parent shell -- meaning that when they exit, all process-local state changes -- including the random number generator's state -- are thrown away.

Reading from $RANDOM inside the parent shell updates the RNG's state, which is why the echo $RANDOM >/dev/null has an effect.

That said, don't do that. Do something like this, which has no subshells at all:

point=
for ((i=0; i<10; i++)); do
  point+=$(( (RANDOM > 16383) ? 0 : 1 ))
done

If you test this generating more than 10 digits -- try, say, 1000, or 10000 -- you'll also find that it performs far better than the original did.

Bevel answered 15/9, 2015 at 4:4 Comment(4)
It was a tough choice between this answer and the other one. The other included the most relevant documentation quote; but I greatly appreciate the alternative code suggestion as well.Dye
Definitely use the subshell-free code; I just focused on explaining the observed behavior.Impulsion
Hmm. Actually, this could be shortened by using a ternary; I'm tempted to revise...Bevel
Could also just do point+=$(( (RANDOM >> 14) && 1 )), and not need the ternary either.Bevel
I
8

From the man page:

The values of RANDOM form an intentionally-repeatable pseudo-random sequence; subshells that reference RANDOM will result in identical pseudo-random values unless the value of RANDOM is referenced or seeded in the parent shell in between subshell invocations.

The "useless" call to echo provides the reference that allows the subshell induced by the command substitution to produce a different value each time.

Impulsion answered 15/9, 2015 at 4:4 Comment(0)
O
1

As other answers have stated, Zsh $RANDOM doesn't behave the same as bash and repeats the same sequence if invoked within a subshell. That behavior can be seen with this simple example:

$ # bash: different numbers; zsh: all the same number
$ for i in {0..99}; do echo $(echo $RANDOM); done
22865
22865
...
22865

What the other answers don't cover is how do get around this behavior in Zsh. While echo $RANDOM >/dev/null helps if you know $RANDOM was invoked, it doesn't help much if it's tucked away in a function you're writing. That puts extra burden on the caller to do this weird echo:

function rolldice {
    local i times=${1:-1} sides=${2:-6}
    for i in $(seq $times); do
      echo $(( 1 + $RANDOM % $sides ))
    done
}

# all 4 players rolled the exact same thing!
for i in {1..4}; do echo $(rolldice 10); done

# seriously 🙄...
for i in {1..4}; do echo $(rolldice 10); echo $RANDOM >/dev/null; done

While it's more expensive (slower) to do, you could get around this by running $RANDOM within a new Zsh session to get results similar to Bash:

function rolldice {
    local i times=${1:-1} sides=${2:-6}
    for i in $(seq $times); do
      echo $(( 1 + $(zsh -c 'echo $RANDOM') % $sides ))
    done
}

# now all 4 players rolled different results!
for i in {1..4}; do echo $(rolldice 10); done

There are other better ways than using $RANDOM to get a uniformly random number in Zsh, but this works in a pinch. One popular alternative is random=$(od -vAn -N4 -t u4 < /dev/urandom); echo $random.

Offense answered 9/9, 2022 at 18:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.