루비 탭 방식의 장점


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
보리스 Stitnicky

28
나는이 사용이 그다지 매력적이지 않다고 생각한다. 틀림없이 더 이상 읽을 수없는 것이기 때문에이 페이지에 있었다. 가독성에 대한 강력한 주장없이 속도를 비교했습니다. 내 테스트는 위의 간단한 구현에 대해 45 % 추가 런타임을 나타내며, 개체의 setter 수가 증가함에 따라 감소합니다. 이들 중 약 10 개 이상이고 런타임 차이는 무시할 수 있습니다 (YMMV). 디버깅 중에 메소드 체인에 '탭핑'하는 것이 승리하는 것처럼 보입니다. 그렇지 않으면 설득이 더 필요합니다.
dinman2022

7
나는 user = User.create!(username: 'foobar')이 경우와 같은 것이 가장 명확하고 짧을 것이라고 생각합니다. :)-질문의 마지막 예입니다.
Lee

4
이 대답은 모순되므로 의미가 없습니다. "이름이 지정된 인스턴스를 만드는 것"보다 더 많은 일이 일어나고 user있습니다. 또한 "독자는 인스턴스 user가 생성 되었음을 알기 위해 블록 내부에있는 내용을 읽을 필요가 없습니다 ." 첫 번째 코드 블록에서 독자는 "인스턴스 user가 생성 되었음을 알기 위해"첫 번째 줄만 읽으면되기 때문에 가중치가 없습니다 .
Jackson

5
내가 왜 여기있는 거지? 왜 우리는 모두 탭이 무엇인지 검색하고 있습니다.
Eddie

37

탭을 사용하는 또 다른 경우는 객체를 반환하기 전에 조작하는 것입니다.

그래서이 대신 :

def some_method
  ...
  some_object.serialize
  some_object
end

추가 줄을 절약 할 수 있습니다.

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

어떤 상황에서이 기술은 한 줄 이상을 절약하고 코드를 더 간결하게 만들 수 있습니다.


24
나는 더 과감한과 같다 :some_object.tap(&:serialize)
amencarini

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
이것은 OP가 이미 그의 질문에 넣은 것의 더 긴 예일뿐입니다. 위의 모든 것을 여전히 할 user = User.new, user.do_something, user.do_another_thing수 있습니다 ... 왜 이렇게 할 수 있는지 확장 해 주시겠습니까?
Matt

예제는 본질적으로 동일하지만 더 긴 형식으로 보여 주면이 경우에 탭을 사용하는 것이 미적으로 더 매력적일 수 있음을 알 수 있습니다. 시연을 돕기 위해 편집을 추가하겠습니다.
Rebitzele 2013-07-05

나도 안 보인다. 사용 tap은 내 경험에 어떤 이점도 추가 한 적이 없습니다. 지역 user변수를 만들고 작업하는 것이 훨씬 더 깨끗하고 제 생각에는 읽기 쉽습니다.
gylaz 2013-07-05

이 두 가지는 동등하지 않습니다. 그렇게 u = user = User.new한 다음 u설정 호출에 사용했다면 첫 번째 예와 더 일치합니다.
Gerry

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하셨나요?
Bryan Ash

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

11

변수의 범위가 실제로 필요한 부분으로 만 제한되기 때문에 코드가 덜 복잡해집니다. 또한 블록 내의 들여 쓰기는 관련 코드를 함께 유지하여 코드를 더 읽기 쉽게 만듭니다.

설명은 tap말한다 :

자신을 블록에 양보 한 다음 자신을 반환합니다. 이 방법의 주요 목적은 체인 내의 중간 결과에 대한 작업을 수행하기 위해 메서드 체인을 "탭"하는 것입니다.

rails 소스 코드에서 tapusage를 검색 하면 흥미로운 사용법을 찾을 수 있습니다. 다음은 사용 방법에 대한 몇 가지 아이디어를 제공하는 몇 가지 항목 (전체 목록이 아님)입니다.

  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. 코드를 더 읽기 쉽게 만들기위한 구문 설탕-아래 예에서 변수 hashserver사용하고 코드의 의도를 더 명확하게 만들 수 있습니다.

    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하면 코드의 의도를 파악 하는 데 도움이됩니다 (반드시 더 간결하게 만들지는 않음).

다음 두 함수는 똑같이 길지만 첫 번째 함수에서는 처음에 빈 해시를 초기화 한 이유를 파악하기 위해 끝까지 읽어야합니다.

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 2015

확실하지이 예제는 더 나은 및 대 벤치마킹 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

이점은 블록이 다른 결과를 반환하더라도 탭이 항상 호출 된 객체를 반환한다는 것입니다. 따라서 흐름을 중단하지 않고 기존 메서드 파이프 라인의 중간에 탭 블록을 삽입 할 수 있습니다.


8

를 사용하는 데 이점이 없다고 말하고 싶습니다 tap. @sawa가 지적했듯이 유일한 잠재적 인 이점 은 "독자는 인스턴스 사용자가 생성되었음을 알기 위해 블록 내부에있는 내용을 읽을 필요가 없습니다."라는 것입니다. 그러나 그 시점에서 단순하지 않은 레코드 생성 논리를 수행하는 경우 해당 논리를 자체 메서드로 추출하여 의도를 더 잘 전달할 수 있다고 주장 할 수 있습니다.

나는 tap코드의 가독성에 대한 불필요한 부담이며 Extract Method 와 같은 더 나은 기술없이 또는 더 나은 기술로 대체 할 수 있다는 의견을 고수합니다 .

하지만 tap편리한 방법이며, 또한 개인적인 취향입니다. 부여 tap시도. 그런 다음 탭을 사용하지 않고 코드를 작성하고 다른 방법보다 좋은지 확인하십시오.


4

우리가 사용할 수있는 용도와 장소는 다양 할 수 있습니다 tap. 지금까지 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 해시 대신 back bar가 표시 되므로 명시 적으로 반환해야합니다. 즉

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 탭
2) 사용해야하는 5 가지 루비 방법


3

당신이 옳습니다 : tap당신의 예에서 사용하는 것은 당신의 대안보다 무의미하고 아마도 덜 깨끗합니다.

Rebitzele이 언급했듯이 tap는 현재 객체에 대한 더 짧은 참조를 만드는 데 자주 사용되는 편리한 방법입니다.

한 가지 좋은 사용 사례 tap는 디버깅입니다. 객체를 수정하고 현재 상태를 인쇄 한 다음 동일한 블록에서 객체를 계속 수정할 수 있습니다. 예를 들어 http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions 를 참조하십시오 .

나는 때때로 tap내부 메서드 를 사용 하여 조건부로 일찍 반환하고 그렇지 않으면 현재 개체를 반환합니다.



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

탭을 사용하여 코드를 모듈화하고 로컬 변수를 더 잘 관리 할 수 ​​있습니다. 예를 들어 다음 코드에서는 메서드 범위에서 새로 만든 개체에 지역 변수를 할당 할 필요가 없습니다. 블록 변수 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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.