Intro

В Ruby есть такой замечательный метод, как #tap, который передает сам объект в качестве аргумента в блок и возвращает сам объект.

x = 1
y = 2.tap { |two| x += two }
[x, y] # [3, 2]

Сегодня обнаружил, что хочу метод, который бы делал то же самое, но возвращал значение из блока. И я вспомнил про такой метод!

Погоди, а для чего это вообще может понадобиться?

Ну, можно часто встретить код вида:

def f
  val = some_object.some_method
  val && some_helper(val)
end

Честно говоря, пример отстой (закон Деметры, ага), но в жизни всякое бывает, и зачастую такого кода не избежать

Собственно, val - промежуточная переменная, которая захламляет пространство имен и мозолит глаза. Было бы здорово иметь возможность сделать что-то вроде:

some_object.some_method.not_a_tap { |x| x && some_helper(x) }

Получается намного лаконичнее, не правда ли?

Метод

Собственно, метод такой есть и очень давно. Это старый добрый #instance_eval:

some_object.some_method.instance_eval { |x| x && some_helper(x) }

# or even with #self
some_object.some_method.instance_eval { self && some_helper(self) }

Но…

Но есть целых две (по моим подсчетам) проблемы.

Экзотичность

Да, увидев такой код, ваши коллеги, скорее всего, будут сначала в небольшом ступоре (хотя лично у меня и #tap до сих пор затуп вызывает), т.к. это не повсеместно используемая практика.

Смена контекста

А вот это проблема посерьезнее. Все дело в том, что #instance_eval выполняет блок (или строку) в котексте объекта. Это значит, что self изменен (выше есть пример), а это в свою очередь означает, что методы объекта будут иметь приоритет над онными в текущем контексте.

Пример:

obj = Object.new
def f; :foo end
def obj.f; :bar end

obj.instance_eval { f } # ==> :bar

Выводы

Мне очень нравится однострочные решения, но использование #instance_eval неоправданно опасно. А жаль.

Надеюсь, что в языке появится альтернатива без смены контекста. Даже создал feature-request в официальном трекере. Вот он