реализовать `each` для Enumerable mixin в Ruby

Я изучаю магию Enumerable в Ruby. Я слышал, что нужно просто включить Enumerable и реализовать метод each, и для этого класса могут быть возможности Enumerable.

Итак, я подумал о реализации собственного пользовательского класса Foo для практики. Это выглядит следующим образом:

class Foo
  include Enumerable

  def initialize numbers
    @num = numbers
  end

  def each
    return enum_for(:each) unless block_given?
    @num.each { |i| yield i + 1 }
  end
end

Этот класс принимает массив, и его each работает почти так же, как Array#each. Вот разница:

>> f = Foo.new [1, 2, 3]
=> #<Foo:0x00000001632e40 @num=[1, 2, 3]>
>> f.each { |i| p i }
2
3
4
=> [1, 2, 3]   # Why this? Why not [2, 3, 4]?

Все работает так, как я ожидаю, кроме одной вещи, которая является последним утверждением. Я знаю, что это возвращаемое значение, но не должно ли оно быть [2, 3, 4]. Есть ли способ сделать это [2, 3, 4].

Также, пожалуйста, прокомментируйте, как я реализовал each. Если есть лучший способ, пожалуйста, дайте мне знать. Сначала в моей реализации у меня не было этой строки return enum_for(:each) unless block_given?, а потом она не работала, когда не было предоставлено никакого блока. Я откуда-то позаимствовал эту строчку, а также, пожалуйста, скажите мне, правильный ли это способ справиться с ситуацией или нет.


person Lokesh    schedule 28.05.2016    source источник


Ответы (4)


Предполагается, что возвращаемое значение each является получателем, то есть self. Но вы возвращаете результат вызова @num.each. Теперь, как я только что сказал, each возвращает self, следовательно, @num.each возвращает @num.

Исправление простое: просто верните self:

def each
  return enum_for(:each) unless block_given?
  @num.each { |i| yield i + 1 }
  self
end

Или, возможно, немного более Rubyish:

def each
  return enum_for(:each) unless block_given?
  tap { @num.each { |i| yield i + 1 }}
end

[На самом деле, начиная с Ruby 1.8.7+, each также должен возвращать Enumerator при вызове без блока, но вы уже правильно с этим справляетесь. Совет: если вы хотите реализовать оптимизированные версии некоторых других методов Enumerable, переопределив их, или хотите добавить свои собственные методы, подобные Enumerable, с аналогичным поведением, что и исходные, вы собираетесь вырезать и вставить ту же самую строку кода. снова и снова, и в какой-то момент вы случайно забудете изменить имя метода. Если вы замените строку на return enum_for(__callee__) unless block_given?, вам не нужно будет помнить об изменении имени.]

person Jörg W Mittag    schedule 28.05.2016

each не изменяет массив. Если вы хотите вернуть измененный массив, используйте map:

def each
  return enum_for(:each) unless block_given?
  @num.map { |i| yield i + 1 }
end
f.each { |i| p i }
2
3
4
=> [2, 3, 4]

Но я рекомендую использовать каждый внутри пользовательского метода. Вы можете увеличить каждый элемент вашего массива на 1 в методе initialize, так как вы хотите использовать его для всех вычислений. Кроме того, вы можете изменить свой метод each, чтобы избежать использования enum_for, передав block_given? внутри блока. В итоге ваш код будет выглядеть так:

class Foo
  include Enumerable

  def initialize(numbers)
    @num = numbers.map {|n| n + 1 }
  end

  def each
    @num.each { |i| yield i if block_given? }
  end
end

f = Foo.new [1, 2, 3]
=> #<Foo:0x00000000f8e0d0 @num=[2, 3, 4]>
f.each { |i| p i }
2
3
4
=> [2, 3, 4]
person Ilya    schedule 28.05.2016
comment
С yield(i+1) if block_given? я получаю неправильный результат для f.each - person Lokesh; 28.05.2016
comment
@Lokesh, возможно, ты пропустил какой-то код. Пожалуйста, смотрите текущую версию ответа - person Ilya; 28.05.2016
comment
Мне очень нравится ваше первое решение. Второе решение, которое вы предложили, которое заключается в изменении инициализации, не соответствует моей потребности, потому что вместо (i+1) у меня есть что-то вроде ('abc' * i), которое занимает место, поэтому изменение @num перед рукой займет на большем пространстве, чем необходимо. - person Lokesh; 28.05.2016
comment
@Lokesh, нет проблем, вставь в каждый: @num.map {|i| i + 1}.each { |i| yield i if block_given? }. Это просто пример, направление, вы же инженер, так что комбинируйте решения!) - person Ilya; 28.05.2016

Вам нужно использовать map вместо each.

f.map { |i| p i }
#=> [2,3,4]

Тот факт, что Foo включает Enumerable, означает, что все методы Enumerable могут быть вызваны для экземпляров Foo.

person Wand Maker    schedule 28.05.2016
comment
Привет. Проблема в том, что я хочу, чтобы мой each вел себя так же, как обычный each. - person Lokesh; 28.05.2016
comment
@Lokesh ведет себя как обычный each. То есть Array ведет себя так же. I делает возможной цепочку методов. map это путь. - person steenslag; 28.05.2016

Этот класс принимает массив, и каждый из них работает почти так же, как Array#each.

Я знаю его возвращаемое значение, но не должно ли оно быть [2, 3, 4].

  1. Def возвращает результат последнего выполненного оператора.

  2. Array#each возвращает исходный массив.

Применение этих правил к вашему определению:

def each
  return enum_for(:each) unless block_given?
  @num.each { |i| yield i + 1 }  #Array#each returns @num
end  #When a block is given, the result of the last statement that was executed is @num

Вы всегда можете сделать что-то вроде этого:

class Foo
  include Enumerable

  def initialize numbers
    @num = numbers
    @enum_vals = []
  end

  def each
    if block_given?
      @num.each do |i| 
        yield i + 1
        @enum_vals << i + 1
      end

      @enum_vals
    else
      enum_for
    end
  end
end

result = Foo.new([1, 2, 3, ]).each {|i| p i}
p result

--output:--
2
3
4
[2, 3, 4]
person 7stud    schedule 28.05.2016