Rubyのthen(yield_self)の概要と具体的な使いどころ

Ruby

then(yield_self)について

thenはレシーバをブロックの引数として受け取り、ブロックの評価をレシーバに反映させるメソッドです。yield_selfthenのエイリアス(等価のメソッド)です。

### thenの挙動を紹介するコードです。実際は『"ruby".upcase』だけで事足ります。
> "ruby".then(&:upcase)
=> "RUBY"

thenとtapの違い

ブロックの結果がレシーバに反映されるのがthen、反映されないのがtapです。

レシーバにブロックの実行結果を反映させたい場合はthenを利用します。
以下はthenを利用して文字列を加工する例です。

> message = "ruby"
> message.then(&:upcase)
         .then(&:reverse)
> message
=> "YBUR"

### 参考: tapはブロックの結果をレシーバに反映させないので変化なし
> message = "ruby"
> message.tap(&:upcase)
         .tap(&:reverse)
> message
=> "ruby"

レシーバにブロックの実行結果を反映させなくてよい場合はtapを利用します。
以下はtapを利用してメソッドチェインの途中結果を出力する例です。

> msg = "ruby"
> new_msg = msg.upcase.tap { |msg| p "1st: #{msg}"} # "1st: RUBY"
               .reverse.tap { |msg| p "2nd: #{msg}" } # "2nd: YBUR"
> new_msg
=> "YBUR"

### 参考: thenだとブロックの結果がレシーバに反映されるため、メソッドチェインに影響を与えてしまう
> msg = "ruby"
> new_msg = msg.upcase.then { |msg| p "1st: #{msg}"} # "1st: RUBY"
               .reverse.then { |msg| p "2nd: #{msg}" } # "2nd: YBUR :ts1"
> new_msg
=> "2nd: YBUR :ts1"

thenの使いどころ

thenの使いどころの具体例を紹介します。

値やインスタンスへの作用のメソッドチェイン化

値やインスタンスに対して次々とメソッドを実行する場合にthenが活躍します。

以下のコードはAPIにアクセスした際のステータスコードを取得する例です。

require "net/http"

url = "https://jsonplaceholder.typicode.com/todos/1"

### URLの文字列からURIインスタンスを作成
uri = URI.parse(url)

### GETリクエストを実行してレスポンスを取得
response = Net::HTTP.get_response(uri)

### レスポンスのコードを取得
response.code
# "200"

上記のコードをthenで書き換えると以下のようになります。thenを利用することで一連の処理をメソッドチェインで表現できます。

require "net/http"

url = "https://jsonplaceholder.typicode.com/todos/1"

url.then(&URI.method(:parse))
   .then(&Net::HTTP.method(:get_response))
   .code # "200"

Active Recordに対する処理のメソッドチェイン化

Active Recordに対してさまざまな処理をする場合にthenが活躍します。

例えばArticleモデルを扱うCollector Objectクラスを作成するとします。サンプルコードは以下の通りです。

class ArticlesCollector
  attr_reader :published, :order_by

  def initialize(published: nil, order_by: nil)
    @published = published
    @order_by = order_by
  end

  def call
    articles = Article.all
    articles = filtering(articles)
    articles = ordering(articles)
    articles
  end

  private

  def filtering(articles)
    return articles if published.blank?
    articles.where(published: true)
  end

  def ordering(articles)
    if order_by == 'title'
      articles.order(:title)
    else
      articles.order(:created_at)
    end
  end
end

上記のcallメソッドはthenを利用すると以下のように簡潔になります。

class ArticlesCollector
  (略)
  def call
    Article.all
           .then(&method(:filtering))
           .then(&method(:ordering))
  end
  (略)
end

なお、上記のCollector Objectの利用方法および実行結果は以下の通りです。

### サンプルデータ
+-------+-----------+---------------------+
| title | published | created_at          |
+-------+-----------+---------------------+
| foo   |         0 | 2021-12-29 06:29:00 |
| bar   |         0 | 2021-12-29 06:29:03 |
| baz   |         1 | 2021-12-29 06:29:10 |
| qux   |         1 | 2021-12-29 06:29:13 |
+-------+-----------+---------------------+
$ rails c

> ArticlesCollector.new.call.map(&:title)
=> ["foo", "bar", "baz", "qux"]

> ArticlesCollector.new(order_by: 'title').call.map(&:title)
=> ["bar", "baz", "foo", "qux"]

> ArticlesCollector.new(published: true, order_by: 'title').call.map(&:title)
=> ["baz", "qux"]

メソッドチェインの途中にロジックを挿入させたいとき

thenを利用することで既存のメソッドチェインの間にロジックを挿入できます。
以下はArticleモデルのメソッドチェインにfilter_byというメソッドをthenを利用して挿入した例です。

class ArticlesController < ApplicationController
  def index
    @articles = Article
                  .where(published: true)
                  .then do |relation|
                    filter_by(
                      relation,
                      params
                    )
                  end
                  .order(:title)
    render json: @articles.map(&:title)
  end

  private

  def filter_by(relation, params)
    if params[:filter] == 'popular'
      relation = relation.where(popular: true)
    end

    if params[:category].present?
      relation = relation.where(category: params[:category])
    end

    relation
  end
end
上記のコードはあくまでthenの活用方法を紹介するための例です。
シンプルな絞り込みを実装する場合は、thenでメソッドを挿入するよりもモデルにscopeを定義するのが一般的です。

なお、上記のindexメソッドのレスポンス例は以下の通りです。

### サンプルデータ
+-----------------+-----------+---------+-------------+
| title           | published | popular | category    |
+-----------------+-----------+---------+-------------+
| qux             |         1 |       0 | NULL        |
| foo_programming |         1 |       1 | programming |
| bar_programming |         1 |       1 | programming |
| bar_diary       |         1 |       1 | diary       |
| baz_programming |         1 |       0 | programming |
+-----------------+-----------+---------+-------------+
### localhostでrails serverが起動している前提
$ curl http://localhost/articles
["bar_diary","bar_programming","baz_programming","foo_programming","qux"]

$ curl http://localhost/articles?filter=popular
["bar_diary","bar_programming","foo_programming"]

$ curl http://localhost/articles?category=programming
["bar_programming","baz_programming","foo_programming"]

$ curl http://localhost/articles?filter=popular&category=programming
["bar_programming","foo_programming"]

時刻の構築

yield_selfを使ったリファクタリングではthenの利用例として『時刻の構築』を挙げています。具体的なコードと実行結果は以下の通りです。詳細については当該記事をご覧になってください。

class TimeConverter
  attr_reader :year, :month

  def initialize(year: nil, month: nil)
    @year = year
    @month = month
  end

  def call
    Time.now.then { |time|
      year ? time.change(year: year) : time
    }.then { |time|
      month ? time.change(month: month) : time
    }
  end
end

$ rails c

> Time.now
=> 2021-12-29 08:00:58.1003061 +0000

> TimeConverter.new.call
=> 2021-12-29 08:01:01.3205858 +0000

> TimeConverter.new(year: 2022).call
=> 2022-12-29 08:01:04.8489875 +0000

> TimeConverter.new(year: 2022, month: 1).call
=> 2022-01-29 08:01:07.699235 +0000

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料