Условно выполнить блок в 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 }