Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Little, shiny robots

One of my favorite computer games when I was growing up was Robot Odyssey; I imagine it will come as a surprise to no one that I was a nerdy kid. This article is a little bit of a tribute to that game, and the coolness of solving complex problems with a handful of simple concepts combined in clever ways.

Imagine you want to write a web-based game that involves robots. The robots in your game are a bit like the robots in Robot Odyssey: you program them with a list of simple instructions and when you turn them on they follow those instructions faithfully. Let’s say, for the sake of argument, that your robots can Walk Forward, Turn Left, Turn Right, Jump, and Beep.

You model each of the moves your Robot can make as an Action, and a Program as a list of Actions. Since each Robot can execute any number of Actions as part of its Program, and any number of Robots can execute Actions, you join Programs and Actions through the Instructions table. Like so:

class Robot < ActiveRecord::Base
  has_one :program
end

class Program < ActiveRecord::Base
  belongs_to :robot
  has_many :instructions
  has_many :actions, :through => :instructions
end

You build a whiz-bang interface for programming each Robot using JavaScript, which will PUT a collection of Action IDs to programs#update (each Robot creates a blank program on initialization, so no need for programs#create).

This seems pretty straightforward, doesn’t it? Unfortunately, it won’t work; the update will fail to reliably record the uploaded Actions. Here are some examples:

@program.action_ids  # []
@program.action_ids = [1, 4] # Forward, Jump

@program.action_ids # [1, 4]
@program.action_ids = [3, 1, 2, 5] # Right, Forward, Left, Beep

@program.action_ids # [3, 1, 2, 5]
@program.action_ids = [5, 5, 5] # Beep, Beep, Beep

@program.action_ids # [5]   ?????

What happened here? The problem lies with the way ActiveRecord collection associations update themselves. Here is the interesting code (slightly paraphrased) from association_collection.rb:

# Replace this collection with +other_array+
# This will perform a diff and delete/add only records that have changed.
def replace(other_array)
  ...
  delete(@target.select { |v| !other_array.include?(v) })
  concat(other_array.select { |v| !@target.include?(v) })
end

As you can see (or you could just read the comment), this only adds an element to a collection if that element isn’t already in the collection (and it acts similarly for deletion). Sadly, it doesn’t take into account how many times an element appears. So, above, when I tried set the action_ids to [5, 5, 5] it saw that the action_ids collection already contained that ID and moved on. If I had set the action_ids collection to [5, 5, 5] when it already contained [1, 4], the result would have been the expected [5, 5, 5].

Now, I’m on the fence with regard to whether I consider this a bug or just a somewhat inconsistent, but expected, behavior. To begin with, the fix is annoyingly nontrivial, and would potentially have a noticeable performance impact. Far more importantly, I’m not sure how often this might reasonably cause a real problem. In the case of your Robots game, you’d probably care quite a lot about what order your Robot executes its Actions in, so you’d likely have a position column, or something similar, on the instructions table. Given that, you’d probably update the Program by sending in a list of nested attributes which would create Instructions, each with the correct position and associated to the correct Action.

Even so, this is behavior worth knowing about. In the infinitude of possible update scenarios someone will want to update their HMT associations this way. I know I initially wrote code to do this as a temporary experiment, and ended up spending the rest of the day trying to figure out why my updates did the wrong thing a small percentage of the time.

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *