Search code examples
rubyfor-loopnomethoderror

undefined method `each' for true:TrueClass (NoMethodError)


I'm writing a for loop in Ruby that iterates through the elements of an array until there are not more elements to read or a condition is met. This is my implementation:

# 'all' is an array

exists = false

for i in all && !exists
  exists = all[i].has_card
end

As the title says, I'm getting this runtime error:

undefined method `each' for true:TrueClass (NoMethodError)

I'm fairly new to Ruby, but I'm guessing the problem is that the for loop is trying to iterate over both all and !exists which is true. How can I write a for that works the way I want?

EDIT: If it serves as clarification, this is how I would implement it in C++:

for (int i=0; i<all.size() && !exists; i++) { /*...*/ }

Solution

  • First of all, eww. for is never used; it's the "moist" of Ruby. :)

    for item in items ... end is executed by Ruby as if you wrote items.each do |item| ... end. (That's actually how Rubyists would write it in the first place.) This is relevant; so hang on.

    Your line parses as:

    for i in (all && (!exists))
    

    When exists is false, !exists is true. Now, the && operator will check if the first argument is truthy; if it is not, it returns it; if it is, it returns the second argument. Since all is presumably truthy, all && true returns the second argument — true.

    Now remember that your line actually, in the background, calls the each method on the collection you are iterating. And your "collection" is true — not a collection at all. You can't iterate on true.

    If you don't want to use break (which is somewhat unreasonable, and you can quote me), you can use other methods from the Enumerable module. For example:

    all.take_while { |item| !item.has_card }.each do |item|
      # ...
    end
    

    This is a bit slower than the break method, since it will construct an intermediary array containing only the first N items that don't have cards that it will later iterate on, but worrying about it is a premature optimisation.

    EDIT: Unfortunately Sebastian deleted their answer, which had the correct idiom (the one I would probably write sooner than the one above, though it definitely depends on the case), so I repeat it here:

    all.each do |item|
      break if item.has_card
      # ...
    end