Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: Object#tap、Object#then を使ってみよう

令和元年最初の年末です。いかがお過ごしでしょうか。私は年末進行まっただ中です😢
今回は聞いたことあるけどあんまり使う機会のなさそうな #tap#yield_self#then の話をしてみたいと思います。

Object#tap

仕様としてはAPIリファレンスを参照すると

self を引数としてブロックを評価し、self を返します。

ということなので、疑似実装としては以下のようになると思います。

class Object
  def tap(&block)
    yield self
    self
  end
end

#tap は古くから存在するメソッドで、1.8.3の頃から存在するようです。

Object#yield_self / Object#then

仕様としてはAPIリファレンスには

self を引数としてブロックを評価し、ブロックの結果を返します。

とあります。擬似的な実装は以下のようになると思います。

class Object
  def yield_self(&block)
    yield self
  end
end

#yield_self は ruby2.5から導入され、#then はそのエイリアスとして ruby2.6 から導入されました。

#then はMatzが直々のコミットです。導入された経緯は RubyKaigi2018のキーノートがとても印象的です(5:32〜)。

両者の違いをみてみる

method 実行内容 戻り値
#tap self を引数としたブロックの評価 self
#then / #yield_self self を引数としたブロックの評価 ブロックの結果

異なるのは戻り値となります。
レシーバー自身が戻るのが #tap
ブロックの評価値が戻るのが #then / #yield_self となります。

実際に使ってみる

では、#tap#then を使ってみて、両者を比較してみましょう。

  • #tap
hash = {}
#  => {}
hash.tap{ |h| h[:value] = 42 }
#  => {:value=>42}
hash
#  => {:value=>42}

  • #then
hash = {}
#  => {}
hash.then{ |h| h[:value] = 42 }
#  => 42
hash
#  => {:value=>42}

このように、ブロックを評価して hash[:value]42 を設定することは同じですが、#tap の場合は、ブロックの内容にかかわらずレシーバー自身が返り、 #then の場合はブロックの評価結果が返ります。

Array#eachArray#map と似てませんか

似てるなと思った方は勘が鋭いです。やってみましょう。

  • Array#each
[1, 2, 3].each{ |i| i*2 }
#  => [1, 2, 3]

ブロックの中で各要素に対する副作用が発生しなければ、レシーバーそのものが返ります。

  • Array#map
[1, 2, 3].map{ |i| i*2 }
#  => [2, 4, 6]

各要素をブロックで評価された、新しいArrayが返ります。

このように、 #tap は、 #each のオブジェクト版。 #then#map のオブジェクト版といえるのではないでしょうか。

うまい使いどころ

#tap#then のうまい使い方をご紹介します。

Railsコンソールでのデバッグ

Railsコンソールには reload! というコマンドがあり、実行するとプロジェクト内のソースコードを読み込み直してくれます。

ところが、変数に設定したオブジェクトの中身までは変更してくれません。

参考: Rails consoleでreload!する場合の注意点 - Qiita

これを回避してみたいと思います。

Railsコンソールのreload!はオブジェクトに効かない

まずは以下の例を見てみましょう。

以下のようなクラスがあるとします。

class User
  attr_accessor :age
end

Railsコンソールで以下のように実行します。

# (rails console)
> user = User.new
> user.age = 30
> user.age
#  ==> 30

Railsコンソールを開いたままで User クラスを変更します。

class User
  attr_accessor :age
  def age
    '18歳だよ'
  end
end 

Railsコンソールで reload! して #age の振る舞いを見てみます

> reload!
> user.age
#  ==> 30  

# (直ってないやん。仕方が無いのでオブジェクトの作り直し)
> user = User.new
> user.age
#  ==>  18歳だよ

#tapを使って回避する

変数にオブジェクトを格納するからマズいのです。
#tap を使ってワンライナーにしてみましょう。

> reload! && User.new.tap{ |user| user.age = 30 }.age
#  ==> 30

# (ソースの変更後、上キーを押して履歴実行)
> reload! && User.new.tap{ |user| user.age = 30 }.age
#  ==> 18歳だよ

実装変更の確認を高速にできるようになりました

ViewHelperをスッキリ書く

数値に , をつけてくれる number_with_delimiter というViewHelperがあります。

<%= number_with_delimiter(@number) %>

ところが number_with_delimiter の引数はnilを許容しません。普通ですと

<% if @number_or_nil %>
  <%= number_with_delimiter(@number_or_nil) %>
<% end %>

とか

<%= @number_or_nil.present? ? number_with_delimiter(@number_or_nil) : '' %>

みたいな読むのがつらいコードになっていくと思います。
(カンマ区切りの数値の出力が1箇所だけならいいですね)

#then&. を組み合わせると以下のようにスッキリ書けます。

<%= @number_or_nil&.then{ |num| number_with_delimiter(num) } %>

@number_or_nil が nil の場合、&. の後ろが評価されないため nil となります。
@number_or_nil が 数値の場合、ブロックの評価値である number_with_delimiter の変換結果が返ります。

nilがあり得るオブジェクトのデコレーターを適用したイメージになると思います。

APIで返すべきものを強調する

例えば以下のような実装があるとします

def spaghetti
  pot = Pot.new
  pot.put(water)
  spaghetti = Spaghetti.pickup(300)
  pot.put(spaghetti)
  pot.boil(7.minutes)
end

このコードは、 Pot#boil の評価値が返ります。スパゲッティかもしれませんし、煮え湯かもしれませんし、鍋かもしれませんし、7分かもしれませんし、nilかもしれません。
でも、#spaghetti が返却するべきはスパゲッティです。

たぶんバグってますね。
最終行に spaghetti と書いてもいいのですが、以下のように書いてもよいと思います。

def spaghetti
  Spaghetti.pickup(300).tap do |spaghetti|
    Pot.new
      .tap{ |pot| pot.put(water) }
      .tap{ |pot| pot.put(spaghetti) }
      .boil(7.minutes)
  end
end

とにかく#spaghetti はスパゲッティが返ります。
具体的には、ポットに水を入れて、スパゲッティを入れて、7分間ゆでられています。

#tap#then を見かけたら、結局何が返ってくるか?を頭に入れてから、ブロックの中身を読んでみるとよいと思います。

まとめ

#tap#then は一見複雑そうに見えますが、「まず何が戻るのか見る。それからブロックの中身を読む」を心がけると逆に可読性が上がる場合があります。

必ず可読性が上がるわけではありませんが、使えるかも?とおもったら是非お試しください。

おたより発掘

関連記事

Ruby: eachよりもmapなどのコレクションを積極的に使おう(社内勉強会)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。