Scott_small Scott Gardner 6 posts

The following section of code (excerpted from page 110 depot_g/app/models/cart.rb line 2 in the book, but I’ve included relevant setup code) has_thrown_me_for :a => :loop…


class Cart
    attr_reader :items

    def initialize
        @items = []
    end

    def add_product(product)
        current_item = @items.find {|item| item.product == product}  # say what?!?

Here’s how I understand it:
1. @items is initialized as an empty array
2. add_product receives a product object
3. The @items array is passed to the block
4. Each @items array item is checked to see if its product.id equals the product.id of the product passed to add_product, and if so that matched item is returned to find, which then returns that item to be assigned to current_item

Can someone please confirm that I have this correct? Presuming yes, I still do not understand how the block, which resolves true or false, knows to return the actual item object when a true condition occurs.

Thanks!

 
Samr_small_small Sam Ruby 36 posts

1. @items is initialized as an empty array

Note that this only occurs when the Cart itself is initialized. After that point, @items can change.

2. add_product receives a product object

Yes.

3. The @items array is passed to the block

More precisely, each item in the @items array is passed to the block until an item is found for which the block evaluates to true.

4. Each @items array item is checked …

This is correct.

I still do not understand how the block … knows to return the actual item object

It doesn’t.

The block is a procedure. It is passed as a parameter to the find method. It is an object with a call method. This means that the caller (in this case, the find method) can call it as many times at it likes, with whatever parameters it likes. And that is exactly what the find method does. It calls the block repeatedly with each successive item in the array until it finds one which causes the block to evaluate true. If this occurs, the find method knows which item it passed to the block, and it simply returns that item. If, instead, it exhausts the list, it simply returns nil instead.

 
Scott_small Scott Gardner 6 posts

Thanks for the detailed explanation. I am really just trying to put a handle on this because I realize it’s a key and common usage in Ruby/Rails. I think, for now, I will just have to rely on rote memory of how this works.

Also, can you confirm/correct your statement that this block is in fact an object? According to this explanation on rubylearning.com, a block is not an object unless it is converted to a Proc (e.g., using lambda).

 
Samr_small_small Sam Ruby 36 posts

The block is automatically converted to a Proc, which is an object.

It may help to play with this yourself by adding your own find method to the Array class, thus:

class Array
  def my_find &block
    puts "block class = #{block.class}" 
    for item in self
      puts "testing item: #{item}" 
      if block.call(item) == true
        return item
      end
    end
    return false
  end
end

data = [1, 2, 3, 4, 5]

puts "scanning for a three" 
result = data.my_find {|i| i == 3}
puts "result: #{result}" 
 
Scott_small Scott Gardner 6 posts

That really helped, thanks!

5 posts, 2 voices