views:

48

answers:

2

On runtime, my code often come into an undefined method error for the method mate. As far as I can figure, a Person somehow slips through the cracks sometime along the code's exucution, and manages not to have an allele assigned to it.

Code (disclaimer, not the best formatted):

class Allele
  attr_accessor :c1, :c2

  def initialize(c1, c2)
    @c1 = c1
    @c2 = c2
  end

 #formats it to be readable
 def to_s
  c1.to_s + c2.to_s
 end

 #given Allele a
 def combine(a)
  pick = rand(4)
  case pick
   when 0
    Allele.new(c1,a.c1)
   when 1
    Allele.new(c1,a.c2)
   when 2
    Allele.new(c2,a.c1)
   when 3
    Allele.new(c2,a.c2)
  end
 end
end

class Person
 attr_accessor :allele, :male

 def initialize(allele,male)
    @allele = allele
  @male= male
  end

 #def self.num_people
  #@@num_people
 #end

 def to_s
  "Allele:" + allele.to_s + " | Male:" + male.to_s
 end

 def male
  @male
 end

 def allele
  @allele
 end

 def mate(p)
  if rand(2) == 0
   Person.new(allele.combine(p.allele),true)
  else
   Person.new(allele.combine(p.allele),false)
  end
 end
end

male_array = Array.new
female_array = Array.new
male_child_array = Array.new
female_child_array = Array.new

# EVENLY POPULATE THE ARRAY WITH 5 THAT PHENOTYPICALLY MANIFEST TRAIT, 5 THAT DON'T
# AND 5 GIRLS, 5 GUYS
pheno_dist = rand(5)
#make guys with phenotype
pheno_dist.times { male_array << Person.new(Allele.new(1,rand(2)),true) }
#guys w/o
(5-pheno_dist).times { male_array << Person.new(Allele.new(0,0),true) }
#girls w/ pheno
(5-pheno_dist).times { female_array << Person.new(Allele.new(1,rand(2)),false) }
#girls w/o
pheno_dist.times { female_array << Person.new(Allele.new(0,0),false) }

puts male_array
puts female_array
puts "----------------------"

4.times do
 #mates male with females, adding children to children arrays. deletes partners as it iterates
 male_array.each do
  male_id = rand(male_array.length) #random selection function. adjust as needed
  female_id = rand(female_array.length)
  rand(8).times do
   child = male_array[male_id].mate(female_array[female_id])
   if child.male
    male_child_array << child
   else
    female_child_array << child
   end
  end
  male_array.delete_at(male_id)
  female_array.delete_at(female_id)
 end

 #makes males male children, females female children, resets child arrays
 male_array = male_child_array
 female_array = female_child_array
 male_child_array = []
 female_child_array = []

 puts male_array
 puts female_array
 puts "----------------------"
end

What immediately looks wrong?

+1  A: 

Deleting from an array which you are iterating with each has undefined behavior. Usually the advice is to use Array#delete_if, but I'm not sure how you would use it in this case.

ergosys
The undefined comes up as in `mate': undefined method `allele' for nil:NilClass (NoMethodError), not from the delete call.The mate call is whats throwing it off, I think.
deeb
this is the sort of undefined behavior you can encounter by changing a list under the iterator.
corprew
+3  A: 

As egosys says, you ought not to delete from an array over which you are iterating.

Another problem is in your loop that starts "4.times do". Sometimes the female array is empty, so returns size 0; rand(0) is a random float >= 0 and < 1. Using that as an array index on an empty female_array returns nil, which is then passed to mate.

But there's more than that wrong. You're iterating over male_array using each, but then picking a male at random. That allows some males to mate more than once; others not at all. Similarly, some females get to mate and reproduce more than once in each iteration, others not at all. Is that your intent?

Let's first consider keeping all of the males and females in the same array. This will simplify things. However, since you do need to sometimes find all the males and sometimes all the females, we'll make methods for that:

def males(population)
  population.find_all do |person|
    person.male?
  end
end

def females(population)
  population.find_all do |person|
    person.female?
  end
end

It would be more biologically accurate if males and females should be paired at random, but nobody gets to mate more than once. That's pretty simple:

def random_pairs(males, females)
  males.shuffle[0...females.size].zip(females.shuffle)
end

Then reproduction of a population becomes, simply:

def make_children(male, female)
  # return an array of children
end

def reproduce(population)
  children = []
  random_pairs(males(population), females(population)).each do |male, female|
    children << make_children(male, female)
  end
  children
end

Having such functions, then doing 4 cycles of reproduction would be as simple as this:

people = # ... generate your initial list of people of all sexe.
4.times do
  people = reproduce(people)
end

Since no function modifies the arguments passed to it, you will have no troubles with side-effects.

More could be done in an OO style, for example making Population a first-class object and moving the functions "males", "females", "random_pairs" and "reproduce" into it. I'll leave that as an exercise for the reader.

Wayne Conrad
Explained above and beyond. Great answer.
deeb
Additional note: "shuffle" is not in the standard library. I had to define it as "sort_by { rand }."
deeb
It's in 1.8.7. It think it's a 1.9 feature that's been back-ported to 1.8.7 (considered the "transitional" Ruby, with one foot in 1.8 land and one foot in 1.9). Are you running 1.8.6 or earlier?
Wayne Conrad
Ya, I'm running 1.8.6, I should have said. I got it working anyways though, this was really a GREAT answer.
deeb