преимущество метода крана в рубине

117

Я как раз читал статью в блоге и заметил, что автор использовал tapво фрагменте что-то вроде:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

У меня вопрос, в чем именно преимущество или преимущество использования tap? Не мог я просто сделать:

user = User.new
user.username = "foobar"
user.save!

или еще лучше:

user = User.create! username: "foobar"
Кайл Декот
источник

Ответы:

104

Когда читатели сталкиваются с:

user = User.new
user.username = "foobar"
user.save!

им нужно будет выполнить все три строки, а затем признать, что он просто создает экземпляр с именем user.

Если бы:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

тогда это будет сразу понятно. Читателю не нужно читать, что находится внутри блока, чтобы знать, что экземпляр userсоздан.

Савва
источник
3
@Matt: А также отбросьте все определения переменных, сделанные в процессе, когда блок выполнил свою работу. И если для объекта User.new.tap &:foobar
вызывается
29
Я не считаю это использование очень привлекательным - возможно, не более читабельным, поэтому были на этой странице. Без веских аргументов в пользу удобочитаемости я сравнил скорость. Мои тесты показывают, что для простых реализаций описанного выше время выполнения увеличивается на 45%, уменьшаясь по мере увеличения количества сеттеров на объекте - около 10 или более, а разница во времени выполнения незначительна (YMMV). «подключение» к цепочке методов во время отладки кажется выигрышем, иначе мне нужно больше, чтобы меня убедить.
dinman2022
7
Я думаю, что user = User.create!(username: 'foobar')в этом случае что-то вроде бы было самым ясным и кратким :) - последний пример из вопроса.
Ли
4
Этот ответ противоречит сам себе и поэтому не имеет смысла. Происходит нечто большее, чем просто создание экземпляра с именем user. Также аргумент, что «Читателю не нужно читать то, что находится внутри блока, чтобы узнать, что экземпляр userсоздан». не имеет веса, потому что в первом блоке кода читателю также нужно прочитать только первую строку, «чтобы узнать, что экземпляр userсоздан».
Джексон
5
Тогда зачем я здесь? Почему мы все здесь ищем, что такое тап.
Эдди
38

Другой случай использования касания - это манипулирование объектом перед его возвратом.

Итак, вместо этого:

def some_method
  ...
  some_object.serialize
  some_object
end

мы можем сохранить лишнюю строку:

def some_method
  ...
  some_object.tap{ |o| o.serialize }
end

В некоторых ситуациях этот метод может сэкономить более одной строки и сделать код более компактным.

Мегас
источник
25
Я был бы еще более решительным:some_object.tap(&:serialize)
amencarini 01
28

Использование тапа, как это сделал блоггер, - это просто удобный метод. Возможно, в вашем примере это было излишним, но в тех случаях, когда вы хотите сделать кучу вещей с пользователем, tap, возможно, может обеспечить более понятный интерфейс. Итак, возможно, это может быть лучше в следующем примере:

user = User.new.tap do |u|
  u.build_profile
  u.process_credit_card
  u.ship_out_item
  u.send_email_confirmation
  u.blahblahyougetmypoint
end

Использование приведенного выше позволяет быстро увидеть, что все эти методы сгруппированы вместе, поскольку все они относятся к одному и тому же объекту (пользователю в этом примере). Альтернативой может быть:

user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint

Опять же, это спорно - но дело может быть сделано, что вторая версия выглядит немного хаотичным, и занимает немного больше человеческого синтаксический, чтобы увидеть, что все эти методы называют на одном объекте.

Ребицеле
источник
2
Это просто более длинный пример того, что OP уже поставил в своем вопросе, вы все равно можете сделать все вышеперечисленное с user = User.new, user.do_something, user.do_another_thing... не могли бы вы расширить, почему можно это сделать?
Мэтт
Хотя пример, по сути, тот же, когда он показывает его в более длинной форме, можно увидеть, как использование крана может быть более эстетичным для этого случая. Я добавлю правку, чтобы продемонстрировать.
Rebitzele 05
Я тоже этого не вижу. По tapмоему опыту, использование никогда не приносило никаких преимуществ. На userмой взгляд , создание локальной переменной и работа с ней намного чище и читабельнее.
gylaz 05
Эти два не эквивалентны. Если бы вы это сделали, u = user = User.newа затем использовали uдля вызовов настройки, это больше соответствовало бы первому примеру.
Джерри
27

Это может быть полезно при отладке серии ActiveRecordсвязанных областей видимости.

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" } 
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

Это упрощает отладку в любой точке цепочки без необходимости хранить что-либо в локальной переменной и не требуя значительного изменения исходного кода.

И, наконец, используйте его как быстрый и ненавязчивый способ отладки, не нарушая нормального выполнения кода :

def rockwell_retro_encabulate
  provide_inverse_reactive_current
  synchronize_cardinal_graham_meters
  @result.tap(&method(:puts))
  # Will debug `@result` just before returning it.
end
Gupta
источник
14

Визуализируйте свой пример в функции

def make_user(name)
  user = User.new
  user.username = name
  user.save!
end

Такой подход сопряжен с большим риском обслуживания, в основном с неявным возвращаемым значением. .

В этом коде вы зависите от save!возврата сохраненного пользователя. Но если вы используете другую утку (или ваша текущая эволюционирует), вы можете получить другие вещи, например, отчет о статусе завершения. Поэтому изменения в утке могут нарушить код, чего не произойдет, если вы обеспечите возвращаемое значение простым userили коснитесь.

Я довольно часто видел подобные инциденты, особенно с функциями, в которых возвращаемое значение обычно не используется, за исключением одного темного угла с ошибками.

Неявное возвращаемое значение, как правило, является одним из тех случаев, когда новички склонны ломать вещи, добавляя новый код после последней строки, не замечая эффекта. Они не понимают, что на самом деле означает приведенный выше код:

def make_user(name)
  user = User.new
  user.username = name
  return user.save!       # notice something different now?
end
SystematicFrank
источник
1
Между вашими двумя примерами нет абсолютно никакой разницы. Вы хотели вернуться user?
Брайан Эш
1
Это было его сутью: примеры точно такие же, один только явно говорит о возврате. Его точка зрения заключалась в том, что этого можно было избежать, используя кран:User.new.tap{ |u| u.username = name; u.save! }
Obversity
14

Если вы хотите вернуть пользователя после установки имени пользователя, вам нужно будет сделать

user = User.new
user.username = 'foobar'
user

С tapвы могли бы спасти это неловкое возвращение

User.new.tap do |user|
  user.username = 'foobar'
end
Монреальмайк
источник
1
Object#tapДля меня это самый распространенный вариант использования .
Lyndsy Simon
1
Что ж, вы сохранили ноль строк кода, и теперь, когда я смотрю на конец метода в поисках того, что он возвращает, мне приходится сканировать назад, чтобы увидеть, что это блок #tap. Не уверен, что это какая-то победа.
Irongaze.com
возможно, но это легко может быть 1 лайнер user = User.new.tap {|u| u.username = 'foobar' }
lacostenycoder
12

В результате код становится менее загроможденным, поскольку объем переменной ограничивается только той частью, где это действительно необходимо. Кроме того, отступ внутри блока делает код более читабельным, поскольку соответствующий код хранится вместе.

Описание tapговорит :

Поддает себя блоку, а затем возвращает себя. Основная цель этого метода - «подключиться» к цепочке методов для выполнения операций с промежуточными результатами внутри цепочки.

Если мы поищем исходный код rails для tapиспользования , мы сможем найти некоторые интересные применения. Ниже приведены несколько пунктов (не исчерпывающий список), которые дадут нам несколько идей о том, как их использовать:

  1. Добавить элемент в массив на основе определенных условий

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
  2. Инициализация массива и его возврат

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
  3. Как синтаксический сахар , чтобы сделать код более читаемым - Можно сказать, что в приведенном ниже пример, использование переменных hashи serverделает цель коды четкой.

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
  4. Инициализировать / вызывать методы для вновь созданных объектов.

    Rails::Server.new.tap do |server|
       require APP_PATH
       Dir.chdir(Rails.application.root)
       server.start
    end

    Ниже пример из тестового файла

    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
  5. Чтобы действовать в соответствии с результатом yieldвызова без использования временной переменной.

    yield.tap do |rendered_partial|
      collection_cache.write(key, rendered_partial, cache_options)
    end
Изготовитель жезлов
источник
9

Вариант ответа @awa:

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

Следующие две функции имеют одинаковую длину, но в первой вам нужно прочитать до конца, чтобы понять, почему я инициализировал пустой хеш в начале.

def tapping1
  # setting up a hash
  h = {}
  # working on it
  h[:one] = 1
  h[:two] = 2
  # returning the hash
  h
end

Здесь, с другой стороны, вы с самого начала знаете, что инициализируемый хеш будет выходом блока (и, в данном случае, возвращаемым значением функции).

def tapping2
  # a hash will be returned at the end of this block;
  # all work will occur inside
  Hash.new.tap do |h|
    h[:one] = 1
    h[:two] = 2
  end
end
Джузеппе
источник
это применение tapдает более веский аргумент. Я согласен с другими, что когда вы видите user = User.new, намерение уже ясно. Однако анонимная структура данных может использоваться для чего угодно, и tapметод, по крайней мере, дает понять, что структура данных является фокусом метода.
volx757 04
Не уверен, что этот пример лучше, и сравнительный анализ def tapping1; {one: 1, two: 2}; endс использованием шоу .tapв этом случае примерно на 50% медленнее
lacostenycoder
9

Это помощник для цепочки вызовов. Он передает свой объект в данный блок и после завершения блока возвращает объект:

an_object.tap do |o|
  # do stuff with an_object, which is in o #
end  ===> an_object

Преимущество заключается в том, что tap всегда возвращает объект, к которому он был вызван, даже если блок возвращает какой-то другой результат. Таким образом, вы можете вставить блок отвода в середину существующего конвейера метода, не прерывая поток.

Пушп Радж Саураб
источник
8

Я бы сказал, что пользы от использования нет tap. Единственное потенциальное преимущество, как указывает @sawa, - цитирую я: «Читателю не нужно читать то, что находится внутри блока, чтобы знать, что создан пользователь экземпляра». Однако в этот момент можно привести аргумент, что если вы используете неупростую логику создания записи, ваше намерение будет лучше передано путем извлечения этой логики в отдельный метод.

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

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

gylaz
источник
4

Может быть множество применений и мест, где мы сможем их использовать tap. Пока я нашел только следующие 2 использованияtap .

1) Основная цель этого метода - подключиться к цепочке методов для выполнения операций с промежуточными результатами в цепочке. т.е.

(1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
    tap    { |x| puts "array: #{x.inspect}" }.
    select { |x| x%2 == 0 }.
    tap    { |x| puts "evens: #{x.inspect}" }.
    map    { |x| x*x }.
    tap    { |x| puts "squares: #{x.inspect}" }

2) Вы когда-нибудь вызывали метод для какого-то объекта, а возвращаемое значение не соответствовало вашим ожиданиям? Возможно, вы хотели добавить произвольное значение к набору параметров, хранящемуся в хэше. Вы обновляете его с помощью Hash. [] , Но вы получаете полосу возврата вместо хэша params, поэтому вы должны вернуть его явно. т.е.

def update_params(params)
  params[:foo] = 'bar'
  params
end

Чтобы преодолеть эту ситуацию здесь, tapв игру вступает метод. Просто вызовите его на объекте, затем передайте касание блока с кодом, который вы хотите запустить. Объект будет передан блоку, а затем будет возвращен. т.е.

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

Есть десятки других вариантов использования, попробуйте найти их сами :)

Источник:
1) API Dock Object Tap
2) пять рубиновых методов, которые вы должны использовать

Аамир
источник
3

Вы правы: использование tapв вашем примере бессмысленно и, вероятно, менее чисто, чем ваши альтернативы.

Как отмечает Ребицеле, tapэто всего лишь удобный метод, часто используемый для создания более короткой ссылки на текущий объект.

Один хороший вариант использования tap- отладка: вы можете изменить объект, распечатать текущее состояние, а затем продолжить изменение объекта в том же блоке. См., Например, здесь: http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions .

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

Джейкоб Браун
источник
Это также приложение, упомянутое в документации: ruby-doc.org/core-2.1.3/Object.html#method-i-tap
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3

Существует инструмент под названием flog, который измеряет, насколько сложно прочитать метод. «Чем выше оценка, тем труднее код».

def with_tap
  user = User.new.tap do |u|
    u.username = "foobar"
    u.save!
  end
end

def without_tap
  user = User.new
  user.username = "foobar"
  user.save!
end

def using_create
  user = User.create! username: "foobar"
end

и, судя по результату, метод с tapнаиболее трудным для чтения (и я согласен с этим)

 4.5: main#with_tap                    temp.rb:1-4
 2.4:   assignment
 1.3:   save!
 1.3:   new
 1.1:   branch
 1.1:   tap

 3.1: main#without_tap                 temp.rb:8-11
 2.2:   assignment
 1.1:   new
 1.1:   save!

 1.6: main#using_create                temp.rb:14-16
 1.1:   assignment
 1.1:   create!
Евморов
источник
1

Вы можете сделать свои коды более модульными, используя tap, и добиться лучшего управления локальными переменными. Например, в следующем коде вам не нужно назначать локальную переменную вновь созданному объекту в области действия метода. Обратите внимание, что переменная блока u ограничена рамками блока. На самом деле это одна из прелестей кода Ruby.

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end
user3936126
источник
1

В рельсах мы можем tapявно использовать для белого списка параметров:

def client_params
    params.require(:client).permit(:name).tap do |whitelist|
        whitelist[:name] = params[:client][:name]
    end
end
Ашан Приядаршана
источник
1

Приведу другой пример, который я использовал. У меня есть метод user_params, который возвращает параметры, необходимые для сохранения для пользователя (это проект Rails)

def user_params
  params.require(:user).permit(
    :first_name,
    :last_name,
    :email,
    :address_attributes
  )
end

Вы можете видеть, что я ничего не возвращаю, но ruby ​​возвращает результат последней строки.

Затем, через какое-то время, мне нужно было условно добавить новый атрибут. Итак, я изменил это примерно так:

def user_params 
  u_params = params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  )
  u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  u_params
end

Здесь мы можем использовать кран, чтобы удалить локальную переменную и удалить возврат:

def user_params 
  params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  ).tap do |u_params|
    u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  end
end
рубиновый принц
источник
1

В мире, где шаблон функционального программирования становится лучшей практикой ( https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming ), вы действительно можете видеть tap, как mapна одном значении , чтобы изменить ваши данные в цепочке преобразований.

transformed_array = array.map(&:first_transformation).map(&:second_transformation)

transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)

Здесь не нужно заявлять itemнесколько раз.

Огюстен Ридингер
источник
0

В чем разница?

Разница в удобочитаемости кода чисто стилистическая.

Прохождение кода:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

Ключевые моменты:

  • Обратите внимание, как uпеременная теперь используется как параметр блока?
  • После завершения блока user переменная теперь должна указывать на пользователя (с именем пользователя: 'foobar', который также сохраняется).
  • Просто приятно и легче читать.

Документация по API

Вот легкая для чтения версия исходного кода:

class Object
  def tap
    yield self
    self
  end
end

Для получения дополнительной информации перейдите по этим ссылкам:

https://apidock.com/ruby/Object/tap

http://ruby-doc.org/core-2.2.3/Object.html#method-i-tap

BKSpurgeon
источник