令和元年最初の年末です。いかがお過ごしでしょうか。私は年末進行まっただ中です😢
今回は聞いたことあるけどあんまり使う機会のなさそうな #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#each
、Array#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: Object#tap、Object#then を使ってみよう|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社 https://t.co/zJFkIGYSwV— カズヒロ (@kazuhiro2nd) April 29, 2020