Условно выполнить блок в Ruby, если значение не равно нулю? (aka Smalltalk ifNotNilDo:)
В Smalltalk существует метод ifNotNilDo:
Он используется следующим образом:
database getUser ifNotNilDo: [:user | Mail sendTo: user ]
В объектах, которые не являются nil
, выполняется блок, передавая сам объект в качестве параметра. Реализация в классе UndefinedObject
(эквивалент Smalltalk Ruby NilClass
) просто ничего не делает. Таким образом, если получение пользователя привело к объекту nil
, ничего не произойдет.
Мне не известно о чем-то подобном для Ruby, поэтому я выкатил свое решение.
Это происходит следующим образом:
class Object
def not_nil
yield(self)
end
end
class NilClass
def not_nil
# do nothing
end
end
Его можно использовать следующим образом:
users = {:peter => "[email protected]", :roger => "[email protected]" }
users[:roger].not_nil {|u| Mail.send(u) }
Это избавляет нас от доступа к хешу дважды
Mail.send(users[:roger]) if users[:roger]
... или используя временную переменную:
u = users[:roger]
Mail.send(u) if u
Обновление:
Люди начинают предлагать решения, основанные на хеш-операциях, а также доступ к хэшу дважды. Мой вопрос не связан непосредственно с хешем.
Представьте себе, что первая операция не является хеш-доступом, а также дорогой. Как:
RemoteUserRepo.find_user(:roger).not_nil {|u| Mail.send(u) }
(с истекшим обновление)
Мои вопросы:
- Неправильно ли я изобретать эту идиому?
- Есть ли что-то вроде этого (или лучше), которое поддерживается в Ruby из коробки?
- Или есть другой, более короткий, более элегантный способ сделать это?
Ответы
Ответ 1
В ActiveSupport существует метод try
.
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
data = { a: [1,2,3] }
data[:a].try(:first)
#=> 1
data[:b].try(:first)
#=> nil
data[:b].first
#=> Exception
Под капотом он реализован рядом с вашим решением. Для любого объекта, но ноль, он "отправит сообщение" (с точки зрения Smalltalk) с атрибутами.
# object.rb
def try(*a, &b)
if a.empty? && block_given?
yield self
else
public_send(*a, &b) if respond_to?(a.first)
end
end
# nilclass
def try(*args)
nil
end
О ваших вопросах
Неужели я ошибаюсь, чтобы передумать эту идиому?
Рельсы ребята сделали что-то подобное
Есть ли что-то вроде этого (или лучше), которое поддерживается в Ruby из коробки?
Нет, Ruby не поддерживает его вне коробки
Или есть другой, более короткий, более элегантный способ сделать это?
По-моему, у него есть проблема: программист должен контролировать данные. Нужно знать, какие данные у него есть, обрабатывать каждый тип и каждый случай или вызывать ошибку. В вашем случае это допустимо для всех типов данных, кроме NilClass. Что может привести к ошибкам, которые очень сложно отладить.
Я предпочитаю использовать старомодный
Mail.send(users[:roger]) if users[:roger]
# or
users[:roger] && Mail.send(users[:roger])
# or use caching if needed
Ответ 2
В Ruby 2.3.0+ вы можете использовать безопасный навигационный оператор (&.
) в комбинации с Object#tap
:
users[:roger]&.tap { |u| Mail.send(u) }
Ответ 3
Вы можете использовать tap
, чтобы избежать двух хеш-доступа:
users[:roger].tap { |u| Mail.send(u) if u }
Я мог бы использовать что-то вроде этого:
[ users[:roger] ].reject(&:nil?).each { |u| Mail.send u }
Ответ 4
В функциональных языках, таких как Ruby, есть идиоматическое решение, которое использует тот факт, что операторы присваивания возвращают значения, которые можно протестировать:
unless (u = users[:roger]).nil?
Mail.send(u)
end
Таким образом, вы избегаете дополнительного поиска хэша, если хотите. (Некоторые функциональные пуристы не одобряют подобную вещь, однако, поскольку она проверяет возвращаемое значение побочного действия).
Ответ 5
Нет, вы не ошибаетесь, чтобы передумать эту идиому. Возможно, вам все же лучше дать более точное имя, возможно if_not_nil
.
Да, есть основной способ Ruby сделать это, хотя он не совсем сочится с элегантностью:
[RemoteUserRepo.find_user(:roger)].compact.each {|u| Mail.send(u)}
Вспомним, что compact
возвращает копию массива с удаленными элементами nil
.
Ответ 6
users.delete_if { |_, email| email.nil? }.each { |_, email| Mail.send email }
или
users.values.compact.each { |email| Mail.send email }