Почему `.each` возвращает nil при вызове ленивого перечисления после`.select`?

У меня есть фрагмент кода, который выглядит следующим образом:

sent_messages = messages.lazy.reject { |m| message_is_spam?(m) }
                             .each   { |m| send_message(m) }
# Do something with sent_messages...

В некотором контексте: метод message_is_spam? возвращает значение true, если получатель сообщения был отправлен в течение последних 5 минут. Когда messages содержит несколько сообщений для одного и того же получателя, последнее сообщение считается спамом только после отправки первого сообщения. Чтобы последнее сообщение считалось спамом, я лениво отклоняю спам-сообщения и отправляю их.

Я ожидаю, что .each вернет массив, содержащий все элементы, но вместо этого получаю nil. .each всегда возвращает массив, кроме этого сценария:

[].each {}                # => []
[].lazy.each {}           # => []
[].select {}.each {}      # => []
[].lazy.select {}.each {} # => nil

Чтобы добавить к путанице, JRuby возвращает [] во всех примерах выше.

Почему .each возвращает nil при вызове так называемый? Я не могу найти что-либо в документах об этом, и трудно понять, что происходит в C-коде.

Я уже понял способ полностью обойти эту проблему; если я выбираю до 1 сообщения на каждого получателя (messages.uniq_by(&:recipient)), операция больше не должна быть ленивой. Тем не менее, это все еще меня удивляет.

Ответы

Ответ 1

Возможное объяснение

Одна из целей Enumerator::Lazy заключается в том, чтобы избежать огромного (или, возможно, бесконечного) массива в памяти. Это может объяснить, почему Enumerator#each не возвращает желаемый массив.

Вместо того, чтобы рисковать исчерпанием памяти с огромным массивом, методы, такие как Lazy#reject, предпочитают возвращать nil в качестве альтернативного значения ( тот, который возвращается после each):

return lazy_add_method(obj, 0, 0, Qnil, Qnil, &lazy_reject_funcs);

Для сравнения, Enumerable#lazy возвращает:

VALUE result = lazy_to_enum_i(obj, sym_each, 0, 0, lazyenum_size);

Я подозреваю, что различные аргументы:

  • Qnil для reject
  • sym_each для lazy

являются причиной:

  • [].lazy.each {} возвращает []
  • [].lazy.select{}.each {} возвращает nil.

Тем не менее, для each не представляется последовательным возвращать массив или nil .

Альтернативы

каждый

Более сложной альтернативой для вашего кода может быть:

messages = %w(a b c)
messages_to_send = messages.lazy.reject{|x| puts "Is '#{x}' spam?"}
messages_to_send.each{ |m| puts "Send '#{m}'" }
# Is 'a' spam?
# Send 'a'
# Is 'b' spam?
# Send 'b'
# Is 'c' spam?
# Send 'c'

Lazy#reject возвращает a lazy Enumerator, поэтому второй message_is_spam? будет выполнен после первого send_message.

Есть одна проблема, хотя вызов to_a в ленивом перечислителе снова вызовет reject:

sent_messages = messages_to_send.to_a
# Is 'a' spam?
# Is 'b' spam?
# Is 'c' spam?

map и модифицированный метод

Вы также можете вернуть m в конце send_message и использовать Lazy#map:

sent_messages = messages.lazy.reject { |m| message_is_spam?(m) }
                             .map { |m| send_message(m) }.to_a

map должен надежно вернуть нужный объект Enumerator:: Lazy. Вызов Enumerable#to_a гарантирует, что sent_messages является массивом.

map и явный возврат

Если вы не хотите изменять send_message, вы можете вернуться m явно в конце каждой map итерации:

messages = %w(a b c)

sent_messages = messages.lazy.reject{ |m| puts "Is '#{m}' spam?" }
                             .map{ |m| puts "Send '#{m}'"; m }.to_a   
# Is 'a' spam?
# Send 'a'
# Is 'b' spam?
# Send 'b'
# Is 'c' spam?
# Send 'c'

p sent_messages
# ["a", "b", "c"]

Модифицированная логика

Еще одна альтернатива - переопределить логику без lazy:

sent_messages = messages.map do |m|
  next if message_is_spam?(m)
  send_message(m)
  m
end.compact

Ответ 2

Если вы используете .map, он возвращает ожидаемый результат, почему каждый дает nil в этом случае неясно.

 p [1,2,3].lazy.select{|x| x>1 }.map{|x| x}.to_a #=> [2, 3]

или просто

p [1,2,3].lazy.select{|x| x>1 }.to_a #=> [2, 3]

По этому вопросу прочитайте этот http://railsware.com/blog/2012/03/13/ruby-2-0-enumerablelazy/ блог