In Progress
Unit 1, Lesson 1
In Progress

Random Seed

Video transcript & code

In earlier episodes, we've talked about how to get random numbers from Ruby, and how to ensure that they are truly, cryptographically random. But as contradictory as it may sound, sometimes we need random numbers to behave more predictably.

For instance, let's say we're playing around with some code that generates random mazes.

Every time we run it we get a different maze.

require "./maze"


puts Maze.new(width: 10, height: 10).to_s
# => nil

# >> ---------------------
# >> |           |     | |
# >> - --------- --- - - -
# >> |   |       |   |   |
# >> - - - ------- ----- -
# >> | | | |     | |   | |
# >> - --- - --- - - - - -
# >> |   |   | | | | | | |
# >> - - ----- - - - --- -
# >> | |       | | | |   |
# >> - ----- - - - - - ---
# >> | | |   | | | | | | |
# >> - - - ----- - - - - -
# >> |   |   |   | |   | |
# >> - ----- - --- - --- -
# >> | |     | |   | |   |
# >> - - ----- --- - --- -
# >> | |     | |   |   | |
# >> - ----- - - ----- - -
# >> |     |     |       |
# >> ---------------------

But what if we see a particularly interesting maze and want to be able to generate it over again? Maybe we want to step through the code in order to understand how that particular design came about. How can we reproduce a random creation?

The answer is that we need to make use of random seeds. As we talked about previously, ruby's standard pseudorandom number generator is seeded with a strongly random number from the system's entropy source. But after that, it proceeds forward mechanically. The algorithm produces a nice even distribution without identifiable patterns. But given the same seed value, it will always produce the same sequence.

The system's default random number generator is contained in a constant called Random::DEFAULT.

Random::DEFAULT                 # => #<Random:0x007f9c6c8fb7f8>

We can ask this object what it's initial seed was. As we can see, it is different every time we run Ruby.

Random::DEFAULT.seed            # => 130422514844656851045348920844218973837
Random::DEFAULT.seed            # => 132475933376449586508660641321277652157

But not only can we ask, we can also provide a random seed, using the Kernel#srand method. When we seed the system default random number generator with the same number twice, and then ask for some random numbers, we get the same sequence of picks each time.

srand(12345)
rand(10)                        # => 2
rand(10)                        # => 5
rand(10)                        # => 1
rand(10)                        # => 4

srand(12345)
rand(10)                        # => 2
rand(10)                        # => 5
rand(10)                        # => 1
rand(10)                        # => 4

So far we've been using the system default random number generator. But we don't have to use the singleton system generator if we don't want to. And that's a good thing, because when we re-seed the default random number generator, we might be interfering with other, unrelated code which also relies on random numbers.

Instead, we can create our own random number generators, and provide a seed as we create them. As you can see here, given the same seed we used above, it produces the same sequence of numbers.

rng = Random.new(12345)
rng.rand(10)                    # => 2
rng.rand(10)                    # => 5
rng.rand(10)                    # => 1
rng.rand(10)                    # => 4

Back in episode #306, we met #sample method on arrays. Now that you know a little bit more about how random number generation works, you might be wondering where it gets its random-ness from.

Well, by default it uses the system default number generator. But we can also provide our own generator as an optional keyword argument.

[1, 2, 3, 4, 5, 6, 7, 8, 9].sample # => 3

rng = Random.new(12345)

[1, 2, 3, 4, 5, 6, 7, 8, 9].sample(random: rng) # => 3

Let's go to our maze generator now. Let's say we want to be able to reproduce mazes by optionally passing in our own seed value. We can enable this with some small changes.

First, we add a new, optional initialization parameter called seed. We make it default to Random.new_seed, which is the usual default for a random number generator when we don't provide a seed.

We add a new instance variable, and store a new random number generator in it, using the seed to initialize it.

The actual maze generation method uses Array#sample to pick where to move next. We update it to pass the @rng instance variable into the #sample method. Now instead of using the system default random number series, it's using one that's specific to this maze generator.

Finally, we add some extra code to the #to_s method to print the current random seed with every maze generated.

Now when we generate a new maze, we also see the random seed that created it. If we see a maze we like, we can take the random seed from one output, and pass it in again and get the exact same maze.

require "./maze"

class Maze
  # ...
  def initialize(width:, height:, seed: Random.new_seed)
    @width  = width
    @height = height
    @cells  = {}
    @walls  = Hash.new do |h, (cell1, cell2)|
      if cell1.nil? || cell2.nil?
        SideWall.new
      elsif h.key?([cell2, cell1])
        h[[cell2, cell1]]
      else
        h[[cell1, cell2]] = Wall.new
      end
    end
    @rng = Random.new(seed)
    populate
    generate
  end

  # ...

  def generate
    stack = []
    stack.push cell_at(0,0)
    i = 0
    until cells.values.all?(&:visited?)
      if stack.last.unvisited_neighbors.any?
        next_cell = stack.last.unvisited_neighbors.sample(random: @rng)
        stack.push next_cell
        next_cell.visited = true
        wall_between(stack[-2], stack[-1]).open = true
      else
        stack.pop until stack.last.unvisited_neighbors.any? || stack.empty?
      end
      i += 1
    end
  end

  # ...

  def to_s
    result = ""
    result << north_wall_string << "\n"
    each_row do |row|
      result << row_string(row) << "\n"
      result << row_south_wall_string(row) << "\n"
    end
    result << "Seed: #{@rng.seed}\n"
    result
  end
end

puts Maze.new(width: 10, height: 10, seed: 48857389216179225828269858709490177991).to_s

# >> ---------------------
# >> |             |     |
# >> - - ------- - - - ---
# >> | | |   |   | | |   |
# >> - - - - - --- ----- -
# >> | | | |   |         |
# >> - - - ------------- -
# >> | | | |       |     |
# >> - --- - ----- - -----
# >> |   | |     | |   | |
# >> --- - ----- - --- - -
# >> | | |       | | | | |
# >> - - - ------- - - - -
# >> |   | |       |   | |
# >> - --- - ------- --- -
# >> | |   |   |   | |   |
# >> - ------- - - - - - -
# >> |   |   |   | | | | |
# >> --- - - ----- - --- -
# >> |     |       |     |
# >> ---------------------
# >> Seed: 48857389216179225828269858709490177991

One place we can see this technique in use is in the RSpec testing framework. Here are three RSpec examples, which simply print their own name.

require "rspec"

RSpec.describe "stuff" do
  it "example 1" do
    puts "example 1"
    expect(1 + 1).to eq(2)
  end

  it "example 2" do
    puts "example 2"
    expect(2 + 2).to eq(4)
  end

  it "example 1" do
    puts "example 3"
    expect(3 + 3).to eq(6)
  end
end

RSpec makes it possible to run tests in random order, using a command-line option. This is useful for discovering cases where tests are unintentionally dependent on each other.

 $ rspec examples.rb --order rand

Randomized with seed 46127
example 3
.example 1
.example 2
.

Finished in 0.0011 seconds (files took 0.10511 seconds to load)
3 examples, 0 failures

Randomized with seed 46127

$ rspec examples.rb --order rand

Randomized with seed 8853
example 1
.example 2
.example 3
.

Finished in 0.00103 seconds (files took 0.10607 seconds to load)
3 examples, 0 failures

Randomized with seed 8853

$ rspec examples.rb --order rand

Randomized with seed 13947
example 2
.example 3
.example 1
.

Finished in 0.00109 seconds (files took 0.10685 seconds to load)
3 examples, 0 failures

Randomized with seed 13947

Of course, if we find such a case, we'd like to be able to repeat it so we can debug the problem. So RSpec prints out the random seed it used after each randomized run.

If we see a run that we want to repeat, we can just supply the same seed back to RSpec, using the --seed option.

$ rspec examples.rb --seed 13947

Randomized with seed 13947
example 2
.example 3
.example 1
.

Finished in 0.00104 seconds (files took 0.10577 seconds to load)
3 examples, 0 failures

Randomized with seed 13947

So there you go: now you know how to give Ruby objects their own private random number generators, and how to reproduce specific random sequences, using a seed. Happy hacking!

Responses