きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Rubyのtapとthenメソッドを理解するために素振りした

Rubyのメソッドよくわからないものが多いので、素振りします。

今回の記事では、tapメソッドとthenメソッドを理解するために素振りしました。なお、私はJavaをメインとしたエンジニアのため、Javaに置き換えた記載もあります。ご了承ください。

環境

  • Ruby
    • 3.0.2p107
  • RSpec

要約

tapメソッド

returnはself。元のインスタンスに対して副作用を起こします。

Javaに置き換えるとラムダ式のpeekメソッドに似ている。

thenメソッド

returnはBlock。元のインスタンスに対して副作用を起こします。yield_selfメソッドと同じ(エイリアス)。

Javaに置き換えるとラムダ式のmapメソッドに似ている。

ユースケース

  • setterの処理をひとまとめにしたい
  • HTTP通信のRequest/Response Bodyをログに残すために、中間操作を加える

挙動確認

自作クラスでないと挙動確認が難しかったため、開始日と終了日を持ったクラスを用意します。

class Term
  attr_accessor :start_date, :end_date
end

tapメソッド

selfを渡して、selfを返却します。Returnはselfになるため、selfに値をsetしないと変更された状態で返却されません。

Javaだとラムダ式のpeekに似ています。

it 'selfに値をセットしていないので1yearsされていない' do
    time = Time.new
    actual = time.tap { |t| t + 1.years }
    # => 返却されるのはTimeクラス
    expect(actual.year).to eq time.year
end

it 'selfに値をセットしているので1yearsされている' do
    term = Term.new
    time = Time.now

    actual = term.tap{ |t| t.start_date = t.start_date + 1.years }
    # => 返却されるのはTermクラス

    # 副作用も起こす
    expect(term.start_date.year).to eq time.year + 1
    expect(term.end_date.year).to eq time.year

    # 返却もSelf
    expect(actual.start_date.year).to eq time.year + 1
    expect(actual.end_date.year).to eq time.year
end

値をsetする箇所を1つにまとめられるため、複数の値をセットしている場合に処理が散らずに可読性が上がりそうです。

# 特にtapを使わない場合
term.start_date += 1.years

# tapを使っている場合
term.tap do |t|
   t.start_date += 1.years
end

# tapを使って、変更をメソッドにまとめている場合
term.tap(&:start_date_update!)

class Term
  attr_accessor :start_date, :end_date

  def start_date_update!(add_year: 1.years)
    @start_date += add_year
  end
end

thenメソッド

selfを渡して、ブロックの結果を返却します。yield_selfメソッドと同じ挙動をします(エイリアス)。tapbreakを組み合わせても、thenメソッドと同じような挙動を得られるそうです。

Javaだと副作用の有無はありますが、ラムダ式のmapメソッドに似ています。

it '値をセットしていないがthenのブロック結果を返却するので1yearsされている' do
    time = Time.new
    actual = time.then { |t| t + 1.years }
    # => t+1.yearsしたTime型が返却される
    expect(actual.year).to eq time.year + 1
end

it '値をセットしていないがthenのブロック結果を返却するので1yearsされている' do
    term = Term.new
    time = Time.now

    actual = term.then do |t|
        t.start_date = t.start_date + 1.years
    end
    # => ブロックで最後にセットしているTime型が返却される

    # 副作用も起こす
    expect(term.start_date.year).to eq time.year + 1
    expect(term.end_date.year).to eq time.year
    # 返却はブロックの結果(値のセットが最後なので、そうなる)
    expect(actual.year).to eq time.year + 1
end

メソッドチェインを使いつつ、副作用を起こしうることを表現できそうです。

it 'HelloWorldを大文字にして、反転させる' do
    actual = "Hello, world!".then(&:upcase).then(&:reverse)
    expect(actual).to eq "!DLROW ,OLLEH"
end

it 'HelloWorldを大文字にして、反転させる(thenなし)' do
    actual = "Hello, world!".upcase.reverse
    expect(actual).to eq "!DLROW ,OLLEH"
end

tapとthenメソッドを組み合わせる

そのままだとミュータブルになってしまうので、イミュータブルな操作をするために一回clone等のコピーメソッドを仲介すると良いかもしれません。

個人的にはイミュータブルな操作を目指したいのですが、Rubyでイミュータブルにするメリットは分からないので、あまり一般的な使われ方ではないかもしれません。

# ※ cloneはシャローコピーなので、ディープコピーする際は自作メソッドを作ってください
it 'thenでcloneしてtapで値を変更する' do
    term = Term.new
    time = Time.now

    actual = term
                .then(&:clone)
                .tap { |t| t.start_date = t.start_date + 1.years }
    # cloneするからイミュータブルにする
    expect(term.start_date.year).to eq time.year
    expect(term.end_date.year).to eq time.year

    # cloneした結果をtapで返却する
    expect(actual.start_date.year).to eq time.year + 1
    expect(actual.end_date.year).to eq time.year
end

ソースコード

終わりに

個人的には、複数の値を1つのブロック内でsetできて、可読性が上がりそうな点が良いと思いました。

ただ、今のところそれ以外の操作が思いつきません。

うまく使いこなせれば可読性が上がるメソッドだと思うのですが、可読性以外のメリットがわからないのでもう少し使ってから考えたいと思います。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報


教えていただいた情報:

f:id:nainaistar:20211212185142p:plain