Перевага кранового методу в рубіні


116

Я просто читав статтю в блозі і помітив, що автор використав 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"

Відповіді:


103

Коли читачі стикаються:

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
Борис Стітнікі

28
Я не вважаю це використання дуже переконливим - можливо, не читабельніше, тому саме на цій сторінці. Не маючи сильного аргументу читабельності, я порівняв швидкість. Мої тести вказують на 45% додатковий час виконання для простих реалізацій вищезазначеного, зменшуючись у міру збільшення кількості сетерів на об'єкті - приблизно 10 або більше, а різниця в режимі виконання незначна (YMMV). 'вторгнення' у ланцюжок методів під час налагодження здається виграшним, інакше мені потрібно більше переконати.
dinman2022

7
Я думаю, що щось подібне user = User.create!(username: 'foobar')було б в цьому випадку найяснішим і найкоротшим :) - останній приклад із запитання.
Лі

4
Ця відповідь суперечить самій собі, і тому не має сенсу. Відбувається більше, ніж "просто створення екземпляра з назвою user". Також аргумент про те, що "читачеві не доведеться читати те, що знаходиться всередині блоку, щоб знати, що створюється екземпляр user". не має ваги, тому що в першому блоці коду читачеві також потрібно лише прочитати перший рядок "щоб знати, що створений екземпляр user".
Джексон

5
Чому я тоді тут? Чому ми всі шукаємо те, що є краном.
Едді

37

Інший випадок використання tap - це зробити маніпуляцію на об'єкті, перш ніж повернути його.

Тож замість цього:

def some_method
  ...
  some_object.serialize
  some_object
end

ми можемо зберегти додатковий рядок:

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

У деяких випадках ця методика може зберегти більше одного рядка та зробити код більш компактним.


24
Мені було б ще some_object.tap(&:serialize)
драматичніше

28

Використання натискання, як це робив блогер, є просто зручним методом. Можливо, у вашому прикладі було надмірно, але у випадках, коли ви хочете зробити купу речей із користувачем, натисніть, можливо, надасть інтерфейс з більш чистим виглядом. Тож, можливо, це може бути краще на прикладі наступного:

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
Це лише довший приклад того, що вже поставила ОП у своєму питанні, ви все-таки могли б зробити все вищезазначене за допомогою user = User.new, user.do_something, user.do_another_thing... Чи можете ви, будь ласка, розширити, чому можна це зробити?
Метт

Хоча приклад по суті той самий, коли він показує його у більш тривалому вигляді, можна побачити, як використання крана може бути більш естетично привабливим у цьому випадку. Я додам правки, щоб допомогти продемонструвати.
Rebitzele

Я також цього не бачу. Використання tapніколи не додало жодних переваг в моєму досвіді. Створення та робота з локальною userзмінною набагато чистіша і на мою думку читабельна.
gylaz

Ці два не є рівнозначними. Якщо ви це зробили, u = user = User.newа потім використовували uдля встановлення викликів, то це було б більше відповідати першому прикладу.
Джеррі

26

Це може бути корисно при налагодженні ряду 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

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

1
Немає різниці між вашими двома прикладами. Ти мав на увазі повернутися user?
Брайан Еш

1
Це було його суть: приклади точно однакові, один лише явний щодо повернення. Його думка полягала в тому, що цього можна уникнути за допомогою крана:User.new.tap{ |u| u.username = name; u.save! }
Непрохідність

14

Якщо ви хочете повернути користувача після встановлення імені користувача, яке вам потрібно буде зробити

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

З цим tapти зможеш зберегти це незручне повернення

User.new.tap do |user|
  user.username = 'foobar'
end

1
Це єдиний найпоширеніший Object#tapдля мене випадок використання .
Лінді Сімон

1
Ну, ви зберегли нульові рядки коду, і тепер, дивлячись на кінець методу на те, що він повертає, я повинен сканувати назад, щоб побачити, що блок є блоком #tap. Не впевнений, що це будь-яка виграш.
Irongaze.com

можливо, але це легко може бути 1-лайнер user = User.new.tap {|u| u.username = 'foobar' }
лакостенікодер

11

Це призводить до менш захаращеного коду, оскільки область змінної обмежена лише тією частиною, де вона справді потрібна. Також відступ всередині блоку робить код більш читабельним, зберігаючи разом відповідний код.

Опис tapсказаних :

Поступається блоку, а потім повертає себе. Основна мета цього методу - "вступити" в ланцюжок методів, щоб виконувати операції з проміжними результатами всередині ланцюга.

Якщо ми будемо шукати вихідний код рейки для 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

Варіант відповіді на @ sawa:

Як уже зазначалося, використання tapдопомагає з'ясувати наміри вашого коду (не обов'язково роблячи його більш компактним).

Наступні дві функції однаково довгі, але в першій ви повинні прочитати до кінця, щоб зрозуміти, чому я ініціалізував порожній Hash на початку.

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

Не впевнений, що цей приклад є кращим, і порівняльний показник порівняно з показниками 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спробувати. Потім напишіть якийсь код, не використовуючи натискання, подивіться, чи подобається вам один шлях над іншим.


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. [] , Але ви отримуєте зворотний рядок замість хеш-парамів, тому вам доведеться його явно повернути. тобто

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

Щоб подолати цю ситуацію тут, tapграє метод. Просто зателефонуйте на об’єкт, а потім перейдіть до торкання блоку з кодом, який ви хотіли запустити. Об'єкт буде переданий до блоку, після чого буде повернутий. тобто

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

Є десятки інших випадків використання, спробуйте їх знайти самостійно :)

Джерело:
1) Док-об’єкт док-сервісу API
2) методи, що використовуються п'ять рубінів


3

Ви маєте рацію: використання tapвашого прикладу є безглуздим і, ймовірно, менш чистим, ніж ваші альтернативи.

Як зазначає Rebitzele, 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

і відповідно до результату flog, метод із 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 , розміщена в межах блоку. Це насправді одна з красунь рубінового коду.

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end

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

Ви можете бачити, що я не повертаю нічого, але рубін повертає висновок останнього рядка.

Потім, через деякий час, мені потрібно було додати новий атрибут умовно. Отже, я змінив це на щось подібне:

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

Тут ми можемо використовувати tap, щоб видалити локальну змінну та видалити повернення:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.