Avatar of w1zeman1p

w1zeman1p's solution

to Robot Name in the Ruby Track

Published at Jul 13 2018 · 3 comments
Instructions
Test suite
Solution

Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code was written.

Manage robot factory settings.

When robots come off the factory floor, they have no name.

The first time you boot them up, a random name is generated in the format of two uppercase letters followed by three digits, such as RX837 or BC811.

Every once in a while we need to reset a robot to its factory settings, which means that their name gets wiped. The next time you ask, it will respond with a new random name.

The names must be random: they should not follow a predictable sequence. Random names means a risk of collisions. Your solution must ensure that every existing robot has a unique name.

In order to make this easier to test, your solution will need to implement a Robot.forget method that clears any shared state that might exist to track duplicate robot names.

Bonus points if this method does not need to do anything for your solution.


For installation and learning resources, refer to the exercism help page.

For running the tests provided, you will need the Minitest gem. Open a terminal window and run the following command to install minitest:

gem install minitest

If you would like color output, you can require 'minitest/pride' in the test file, or note the alternative instruction, below, for running the test file.

Run the tests from the exercise directory using the following command:

ruby robot_name_test.rb

To include color from the command line:

ruby -r minitest/pride robot_name_test.rb

Source

A debugging session with Paul Blackwell at gSchool. http://gschool.it

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

robot_name_test.rb

require 'minitest/autorun'
require_relative 'robot_name'

class RobotTest < Minitest::Test
  NAME_REGEXP = /^[A-Z]{2}\d{3}$/

  def setup
    Robot.forget
  end

  def test_can_create_a_robot
    skip
    refute_nil Robot.new
  end

  def test_has_name
    skip
    assert_match NAME_REGEXP, Robot.new.name
  end

  def test_name_sticks
    skip
    robot = Robot.new
    original_name = robot.name
    assert_equal original_name, robot.name
  end

  def test_reset_changes_name
    skip
    robot = Robot.new
    original_name = robot.name
    robot.reset
    refute_equal original_name, robot.name
  end

  def test_reset_before_name_called_does_not_cause_an_error
    skip
    robot = Robot.new
    robot.reset
    assert_match NAME_REGEXP, Robot.new.name
  end

  def test_reset_multiple_times
    skip
    robot = Robot.new
    names = []
    5.times do
      robot.reset
      names << robot.name
    end
    # This will probably be 5, but name uniqueness is only a requirement
    # accross multiple robots and consecutive calls to reset.
    assert names.uniq.size > 1
  end

  def test_different_robots_have_different_names
    skip
    refute_equal Robot.new.name, Robot.new.name
  end

  # This test assumes you're using Kernel.rand as a source of randomness
  def test_different_name_when_chosen_name_is_taken
    skip
    same_seed = 1234
    Kernel.srand same_seed
    robot_1 = Robot.new
    name_1  = robot_1.name
    Kernel.srand same_seed
    robot_2 = Robot.new
    name_2 = robot_2.name
    refute_equal name_1, name_2
  end

  def test_generate_all_robots
    skip
    all_names_count = 26 * 26 * 1000
    time_limit = Time.now + 60 # seconds
    seen_names = Hash.new(0)
    robots = []
    while seen_names.size < all_names_count && Time.now < time_limit
      robot = Robot.new
      seen_names[robot.name] += 1
      robots << robot
    end
    timeout_message = "Timed out trying to generate all possible robots"
    assert_equal all_names_count, robots.size, timeout_message
    assert seen_names.values.all? { |count| count == 1 }, "Some names used more than once"
    assert seen_names.keys.all? { |name| name.match(NAME_REGEXP) }, "Not all names match #{NAME_REGEXP}"
  end

  def test_version
    assert_equal 3, BookKeeping::VERSION
  end
end
require 'set'

class RobotNameGenerator
  def next
    [rand_letter, rand_letter, rand_digit, rand_digit, rand_digit].join
  end

  private

  def rand_letter
    rand_in ('A'..'Z')
  end

  def rand_digit
    rand_in ('0'..'9')
  end

  def rand_in(range)
    range.to_a.sample
  end
end

class RobotIndex
  def self.name_generator
    @name_generator ||= RobotNameGenerator.new
  end

  def initialize(name_generator=RobotIndex.name_generator)
    @name_generator = name_generator
    @used_names = Set.new
  end

  def next_name
    begin
      candidate = name_generator.next
    end while used_names.include?(candidate)
    used_names << candidate
    candidate.dup
  end

  def delete(name)
    used_names.delete(name)
  end

  private
  attr_reader :used_names, :name_generator
end

class Robot
  VERSION = 1
  attr_reader :name

  def self.index
    @index ||= RobotIndex.new
  end

  def initialize(index=Robot.index)
    @index = index
    @name = index.next_name
  end

  def reset
    index.delete(@name)
    @name = index.next_name
  end

  private
  attr_reader :name_generator, :index
end

Community comments

Find this solution interesting? Ask the author a question to learn more.
Avatar of w1zeman1p

I decided to create a class RobotIndex specifically for keeping track of the used names. I've injected a RobotNameGenerator into the RobotIndex so that later, if we decide that we want to generate names a different way, we can swap that out pretty easily.

Additionally I've injected the RobotIndex into the Robot with the thought that in the future we could swap out what it means to keep track of names and their uniqueness. Maybe we allow doubles? triples? (Also figured this might be a good way to start the index off with some list of blacklisted names, which i did not implement, but figured it might be a good way to handle?)

I did away with the Enumerator. What I was trying to do was make the RobotNameGenerator an Enumerator its self, but that was unnecessarily adding complexity, so i've ripped that out.

I think by .dup on line 38 I'm allowing the robots to be garbage collected because the name would not be in another data structure? Not sure how to handle removing the name from the used_names at the time of garbage collection.

Had to run, but was thinking of having a class method on RobotNameGenerator that would set srand to control the seed?

Thanks again, @remcopeereboom for pushing me further on this. :)

Avatar of w1zeman1p

forgot to remove name_generator from Robot.

Avatar of remcopeereboom

Wow, this is looking very good. Notice how everything now has a single very clearly defined responsibility! I think it might be time to start decoupling the name from Robot entirely. Having a serial number is something that lot's of objects can use, not just robots. The coupling is mostly absent in the logic, but still very much present in the names of objects.

I've injected a RobotNameGenerator into the RobotIndex so that later, if we decide that we want to generate names a different way, we can swap that out pretty easily.

Again, it would be cleaner to decouple this by having a RobotFactory, but this requires modifying the tests, which you might be unwilling to do on exercism. Given that, I think using a default argument is the best and most flexible solution.

I did away with the Enumerator. What I was trying to do was make the RobotNameGenerator an Enumerator its self, but that was unnecessarily adding complexity, so i've ripped that out.

I figured you were trying to do that. I kind of liked that intent; it made me think of Haskell.

I really like how the methods in your random name generator came out. The methods are now short and specific enough to be useful in many situtations, even in other programs.

was thinking of having a class method on RobotNameGenerator that would set srand to control the seed?

Setting srand won't be sufficient. You cannot guarantee that some other part of the code won't alter the global rng. You need to own your instance of the rng in order to guarantee the seed. That's also why it would be better to have each instance to have its own seed method.

I think by .dup on line 38 I'm allowing the robots to be garbage collected because the name would not be in another data structure? Not sure how to handle removing the name from the used_names at the time of garbage collection.

Even if you did not .dup, the robot and it's reference would still be garbage collected. I like that you .dup, because it is a nice way of guaranteeing that the class invariant (a name occurs only once) is never broken. It comes with some overhead, so there is a security-speed tradeoff here. But why optimize prematurely? Very nice!

Ruby objects don't have destructors, but you can add behaviour on garbage collection by interacting with the ObjectSpace module. You can pass an object and proc to ObjectSpace.define_finalizer. The proc will be called when the object gets garbage collection. Just be EXTREMELY careful: make sure the proc does not maintain a reference to the object, or the object will never get garbage collected!

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?