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メソッドと同じ挙動をします(エイリアス)。tapとbreakを組み合わせても、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
ソースコード
- ror-practice/then_spec.rb at 3232519175d14ace711da802236addea9422a6b8 · hirotoKirimaru/ror-practice · GitHub
- ror-practice/term.rb at 2f7149af42f8d30f27286fab808452d206c1f7df · hirotoKirimaru/ror-practice · GitHub
終わりに
個人的には、複数の値を1つのブロック内でsetできて、可読性が上がりそうな点が良いと思いました。
ただ、今のところそれ以外の操作が思いつきません。
うまく使いこなせれば可読性が上がるメソッドだと思うのですが、可読性以外のメリットがわからないのでもう少し使ってから考えたいと思います。
この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。
参考情報
- tapかわいいよtap - http://rubikitch.com/に移転しました
- Ruby: Object#tap、Object#then を使ってみよう|TechRacho by BPS株式会社
- tap 面白いよ tap - Qiita
- Object#tap (Ruby 3.0.0 リファレンスマニュアル)
- Object#then (Ruby 3.0.0 リファレンスマニュアル)
教えていただいた情報:
tapではオブジェクトに対してもオブジェクトの外に対しても副作用を与えるコードが多い気がしますが、thenで副作用があるとちょっと読みにくいなという感想です。
— Kohei Sugi (@koheiSG) 2021年12月13日
(thenは歴史が浅いので、よく使うかと言われるとそうでも無いかなぁという気がします。私は代入を避けたいときによく使います。)
メソッドチェーンで呼び出せなくて、さらに代入せずに結果値を使いたい時なので稀だと思います。
— Kohei Sugi (@koheiSG) 2021年12月13日
String | nil な 時刻を表す strtime という値に対して、
strtime&.then {|f| Time.parse(f)} でTime型を得るのとかワンライナーとかでif書きたくないので便利ではあります。
