Fun with Ruby Arrays
Last night I lost hours of my life to strange behavior with Ruby arrays. Now that I know what’s going on it all makes sense but I can see how someone can easily be confused by this. I’m sharing it in hopes that others might avoid the same pitfall and as a reminder to myself in the future.
The issue I ran into involves iteration. While iterating over an array I was also selectively deleting that item from the array if it matched some criteria. What I found is that I was ‘randomly’ failing to iterate over some values in my array.
Below is an example of what I was doing. For context, I was writing AWS Network ACL support for chef/chef-provisioning-aws.
current_rules = [
{
rule_number: 100,
action: :deny,
protocol: -1,
cidr_block: '10.0.0.0/24',
egress: false,
port_range: -1
}
]
desired_rules = [
{
rule_number: 100,
action: :deny,
protocol: -1,
cidr_block: '10.0.0.0/24'
},
{
rule_number: 200,
action: :allow,
protocol: -1,
cidr_block: '0.0.0.0/0'
},
{
rule_number: 300,
action: :allow,
protocol: 6,
port_range: 22..23,
cidr_block: '172.31.0.0/22'
}
]
desired_rules.each do |desired_rule|
matching_rule = current_rules
.select { |r| r[:rule_number] == desired_rule[:rule_number]}.first
if matching_rule
# Anything unhandled will be removed
current_rules.delete(matching_rule)
# Anything unhandled will be added
desired_rules.delete(desired_rule)
...
end
end
Since current_rules
and desired_rules
both have rule_number: 100
they are deleted from both arrays.
That seems fine, but what happened to desired_rules
(remember, the array I’m currently iterating over)?
desired_rules
now looks like this:
[
{
rule_number: 200,
action: :allow,
protocol: -1,
cidr_block: '0.0.0.0/0'
},
{
rule_number: 300,
action: :allow,
protocol: 6,
port_range: 22..23,
cidr_block: '172.31.0.0/22'
}
]
All the remaining elements in the array shifted forwarded by one index. Element with
rule_number: 200
shifted from index 1
to index 0
. The next iteration over the array is index 1
, now
rule_number: 300
. Whoops!
Knowing the issue here is most of the battle. The solution is rather easy - just clone the array before iterating:
desired_rules.clone.each do |desired_rule|
...
end
desired_rules
elements still shift one index forward but we’re iterating over a clone that is unaffected by the
delete action.
My friend Stuart (@McCroden) is working with Swift and came across an interesting Stack Overflow answer suggesting reverse iteration to avoid this problem. That’s a neat way to get around this, too.
Special thanks to Christopher Webber (@cwebber) for suggesting this post. It’s a great, small topic to get my blog off the ground. I will remember to blog about small issues like this more in the future.
Unfortunately I don’t have comments enabled on my blog yet. If you have comments, please send them to @drewblessing on Twitter.