Daniel Viglione
Daniel Viglione

Reputation: 9407

Using methods of Enumerable in custom Class

I was experimenting with Enumerable and Comparable. I read the documentation and wanted to try it out.

class Apple
  attr_accessor :color
end

class Fruit
  include Enumerable
  include Comparable

  attr_accessor :apples

  def initialize
    @apples = []
  end

  def <=> o
    @apple.color <=> o.color
  end

  def each
    @apples.each {|apple| yield apple }
  end

  def to_s
    "apple: #{@apple.color}"
  end
end

fruit = Fruit.new
a1 = Apple.new
a1.color = :red
a2 = Apple.new 
a2.color = :green
a3 = Apple.new
a3.color = :yellow
fruit.apples.push a1
fruit.apples.push a2
fruit.apples.push a3

Two things are not working as expected. So I override to_s, I expect each index of array to contain a string like "apple: red". Instead I get this:

fruit.sort
 => [#<Apple:0x007fbf53971048 @apples=[], @color=:green>, #<Apple:0x007fbf53999890 @apples=[], @color=:red>, #<Apple:0x007fbf5409b530 @apples=[], @color=:yellow>]

Second issue is when I include Enumerable, the instance methods of Enumerable should have been added to the ancestor chain right before the inherited classes. This should have included methods of Enumerable like with_each, reduce, etc to the ancestor chain. However, when I do this:

fruit.each.with_index(1).reduce({}) do |acc,(apple,i)|
  acc << { i => apple.color}
end
LocalJumpError: no block given (yield)

as you can see, I get a LocalJumpError. I expected a result like this:

{ 1 => :red, 2 => :green, 3 => :yellow}

What am I doing wrong? I defined each like I was supposed to yet it doesn't work as expected.

Upvotes: 0

Views: 171

Answers (2)

Stefan
Stefan

Reputation: 114158

I override to_s, I expect each index of array to contain a string like "apple: red". Instead I get this: ...

Two things are wrong here.

1) you have to implement Apple#to_s, not Fruit#to_s:

class Apple
  attr_accessor :color

  def to_s
    "apple: #{color}"
  end
end

2) you have to implement inspect or define it as an alias:

class Apple
  attr_accessor :color

  def to_s
    "apple: #{color}"
  end
  alias inspect to_s
end

This will give you:

fruit = Fruit.new
a1 = Apple.new
a1.color = :red
a2 = Apple.new
a2.color = :green
a3 = Apple.new
a3.color = :yellow
fruit.apples.push a1
fruit.apples.push a2
fruit.apples.push a3

fruit
#=> #<Fruit:0x00007faa3686b7c0 @apples=[apple: red, apple: green, apple: yellow]>

Second issue is when I include Enumerable, the instance methods of Enumerable should have been added to the ancestor chain ...

When you write:

fruit.each.with_index(1)

you are calling with_index on the return value of each. That's where the error occurs:

fruit.each
#=> LocalJumpError: no block given (yield)

You have to return an instance of Enumerator when no block is given. This can be achieved using a conditional (see mudasobwa's answer) or by passing the block along:

def each(&block)
  @apples.each(&block)
end

There's another issue with your code: not Fruit but Apple is the class that should implement <=> and include Comparable. Because when sorting @apples, the items are being compared to each other:

class Apple
  include Comparable

  attr_accessor :color

  def <=> o
    color <=> o.color
  end

  # ...
end

Note that there's a catch when including Enumerable. Although you are able to use all those methods, you can easily lose your wrapping class and end up with a plain array:

fruit
#=> #<Fruit:0x00007faa3686b7c0 @apples=[apple: red, apple: green, apple: yellow]>

fruit.sort
#=> [apple: green, apple: red, apple: yellow]

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Return an Enumerator::Lazy#enum_for when no block is given:

def each
  @apples.each
end

Array does that on it’s own, hence the above is already possible. The code inside is effectively similar to:

def each
  return enum_for(:each) unless block_given?

  @apples.each { |apple| yield apple }
end

What you see in pry/irb is the result of inspect, not to_s.

Upvotes: 3

Related Questions