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書きたくないので便利ではあります。