yield_selfを使ったリファクタリング

## 概要

Ruby 2.5で追加された`Kernel#yield_self`を使ったRailsアプリのリファクタリング例を3つ紹介する。また、`Kernel#yield_self`と同一の機能を表す __Tコンビネータ__ を含む __コンビネータ論理__ と、上位互換となる実装についても紹介する。

## yield_selfとは

`yield_self`はブロックを受け取り、そのブロック引数に"自分"を渡すメソッドです。例えば下のコードでは

```ruby
(a - b).yield_self{|n|
  n > 0 ? n : a
}
```
`(a - b)`の結果が`n`となり、ブロックの実行結果が最終的な値となります。同じことを`yield_self`を使わずに書くと、`a - b`の結果を保存する変数を（ブロックの外側に）用意するか、`(a - b)`を2回書かなければなりません。

以下では`yield_self`をRailsアプリのリファクタリングに使う実践的な例を見ていきます。

## 例1. フィルタリングとソートをするindexアクション

下のindexアクションはHTTPパラメータに従ってモデルのscopeを使い分けます。

```ruby
class ProductsController < ApplicationController
  def index
    products = Product.all

    products = case params[:filter]
      when 'popular'
        products.liked_at_least(10)
      when 'liked'
        products.liked_by(current_user)
      else
        products
      end

    products = case params[:order]
      when 'likes'
        products.popularity_order
      when 'release'
        products.newer_first
      else
        products.recommendation_order
      end

    @products = products.page(params[:page])
  end
end
```

このコードには幾つか問題がありますが、まず2つのcase文は`index`メソッド全体から見ると処理の粒度が小さいので、独立したメソッドに展開します。

```ruby
class ProductsController < ApplicationController
  def index
    products = Product.all
    products = filter(products)
    products = order(products)
    @products = products.page(params[:page])
  end

  private

  def filter(products)
    case params[:filter]
    when 'popular'
      products.liked_at_least(10)
    when 'liked'
      products.liked_by(current_user)
    else
      products
    end
  end

  def order(products)
    case params[:order]
    when 'likes'
      products.popularity_order
    when 'release'
      products.newer_first
    else
      products.recommendation_order
    end
  end
end
```

`products = `が並んでしまいました。このような再代入は変数の役割を曖昧で分かりにくくします。つまり最初の代入の時点では全てのProduct、次の代入ではフィルタリングしたProduct、3番目の代入ではさらに並べ替えを施したProductと、別の値なのにすべて`products`と呼んでいます。

代入をネガティブに評価する例は他にもあります。例えばコードの読み難さを評価する静的解析ツール[Flog](http://ruby.sadi.st/Flog.html)では、条件分岐と同等の減点です。

またソフトウェアのサイズを評価するABC Metricでも条件分岐と同じ重み付けです。ソフトウェアのサイズが増せばバグの発生率も上がるので、ネガティブな評価と言えるでしょう。

では代入を減らすことを考えます。下のようにも書けますが、

```ruby
  def index
    @products = order(filter(Product.all)).page(params[:page])
  end
```

より一般的な`Model.scope`の語順で書いた方が多くの人に読みやすいでしょう。そこで`yield_self`を使うと下のように書けます。

```ruby
  def index
    @products = Product
      .all
      .yield_self(&method(:filter))
      .yield_self(&method(:order))
      .page(params[:page])
  end
```

## 例2. 時刻の構築

次は受け取ったパラメータに従って年と月を決定する処理です。ただしパラメータを受け取らない場合もあり、仕様は以下の通りです：

* デフォルトは処理時点の時刻
* 年を受け取ったら、指定の年の始めにリセットする
* 月を受け取ったら、指定の月の始めにリセットする

元のコードは下の通りです：

```ruby
def time
  t = Time.now
  t = t.change(year: params[:year]) if params[:year]
  t = t.change(month: params[:month]) if params[:month]
  t
end
```

例1と同様に再代入を無くしたいので`yield_self`を使い、以下のようになります。

```ruby
def time
  Time.now.yield_self {|time|
    params[:year]  ? time.change(year: params[:year])   : time
  }.yield_self {|time|
    params[:month] ? time.change(month: params[:month]) : time
  }
end
```

## 例3. 複数の正規化処理

次の例は`params`に複数の正規化を施すものです。背景として、様々なバージョンのAPIクライアントが存在し、同じデータをバージョンによって別の名前で送ってくると考えてください。その差異を吸収するクラス`UserForm`の実装です。

外から見える部分の実装は下の通りです：

```ruby
class UserForm
  delegate :save, to: :@user

  def initialize(user)
    @user = user
  end

  def assign_attributes(params)
    @user.assign_attributes(normalize(params)[:user])
  end

  # 以下略
```

つまりUserオブジェクトを渡してインスタンス化し、バージョン不明の`params`を`assign_attributes(params)`すれば、差異を吸収して処理してくれるということです。使用例は次の通り。

```ruby
  form = UserForm.new(User.new)
  form.assign_attributes(params)
  if form.save
    # 以下略
```

次に`UserForm`が担う正規化処理です。まず古いAPIでは`params[:avatar]`だったものが`params[:user][:avatar]`に変わったことを吸収するメソッドです。

```ruby
  def normalize_avatar(params)
    params[:user][:avatar] = params.delete(:avatar) if params[:avatar]
    params
  end
```

次に`params[:user][:profile]`が`params[:user][:profile_attributes]`に変わったことに対応するメソッドです。

```ruby
  def adjust_association_attributes(params)
    params[:user][:profile_attributes] = params[:user].delete(:profile) if params[:user][:profile]
    params
  end
```

この2つの処理を行うのが、次の`normalize`メソッドです：

```ruby
  def normalize(params)
    adjust_association_attributes(normalize_avatar(params))
  end
```

この`normalize`メソッドは「paramsに対してnormalize_avatarし、さらにadjust_association_attributesする」ものですが、書かれている順序が逆です。そこで`yield_self`を使い、次のようにリファクタします。

```ruby
  def normalize(params)
    params
      .yield_self(&method(:normalize_avatar))
      .yield_self(&method(:adjust_association_attributes))
  end
```

これで思考の順序と記述の順序が一致しました。

リファクタリング例は以上にして、以下では`yield_self`の背景を掘り下げていきます。

## yield_selfはTコンビネータ

`Kernel#yield_self`はコンビネータ論理においてTコンビネータと呼ばれる機能です。コンビネータ論理は値と関数を区別しないシンプルな記法が特長の数学的な論理体系で、1920年代にSchonfinkelによって考案されました。コンビネータ論理においてTコンビネータは以下のように定義されます。

    Txy = yx

つまりTにxを与え、さらにyを与えることが、yにxを与えることと等しい、という意味です。`yield_self`にとってはxがレシーバ、yが引数です。つまりyがProcオブジェクトのとき、

```ruby
x.yield_self(&y)
```
が

```ruby
y.(x)
```
と等しいという意味ですから、正にTコンビネータです。

## tapはKコンビネータ

実は`Kernel#tap`はKコンビネータと呼ばれています。Kコンビネータの定義は下のようになります。

    Kxy = x

Rubyで言えば、どんなレシーバxに対して`x.tap(y)`を実行してもxを返す、となります。

## コンビネータと鳥

TやKというアルファベットは鳥の名前に由来しています。TはThrush（ツグミ）、KはKestrel（チョウゲンボウ）です。パズルを通じてコンビネータ論理を紹介する[To Mock a Mocking Bird](http://amzn.to/2irVBJ5)という本には、この2種類を含め人間の呼びかけに応える能力を持った鳥が24種類と、それらの亜種が登場します。

これらの鳥に何らかの鳥の名前を言うと、その性質に沿った名前を返します。例えば自分の名前を呼ばれると自分の名前を返す鳥は **自己中心的(egocentric)** と言われ、次のように表されます。

    xx = x

コンビネータ論理においても`()`は優先を表します。`Kxy`は`(Kx)y`の省略であり、Kに「x」と呼びかけたとき得られた鳥に「y」と呼びかけることを表します。

Starling（ムクドリ）は下の性質を持っており、

    Sxyz = xz(yz)

その他すべての鳥はKとSで再現できることが分かっています。

気づいた人もいると思いますが、これらの鳥は以下の点が高階関数と共通しています。

* 値を渡して呼べる
* 戻り値に対し、別の値を渡して呼べる

コンビネータ論理を知ることで高階関数に対する理解が深まると思いますので、興味のある方は調べてみてください。

## yield_selfの改良

実は`yield_self`の上位互換のメソッド`into`が6年以上前に[発表されています](https://github.com/raganwald-deprecated/homoiconic/blob/master/2008-10-30/thrush.markdown#readme)。その実装は以下の通りです（ライセンスについては[オリジナルのページ](https://github.com/raganwald-deprecated/homoiconic/blob/409f15fee14627e295f2016fdab9f792a8709e8d/2008-10-30/into.rb)を参照してください）。

```ruby
class Object
  def into expr = nil
    expr.nil? ? yield(self) : expr.to_proc.call(self)
  end
end
```

この実装が`yield_self`より優れているのは、ブロック以外の引数を渡したとき`.to_proc`してくれる点です。この実装を使うと例1のコードは以下のようになります。

```ruby
  def index
    @products = Product
      .all
      .into(method :filter)
      .into(method :order)
      .page(params[:page])
  end
```

また例3のコードは下のようになります。

```ruby
  def normalize(params)
    params
      .into(method :normalize_avatar)
      .into(method :adjust_association_attributes)
  end
```

`&`と`()`が不要でメソッド名も短く、個人的にはこちらの方が読みやすいと感じます。5年前に`yield_self`のアイデアが挙がり、その[4日後には名前の議論が始まっていた](https://bugs.ruby-lang.org/issues/6721#note-4)ので、議論に加わらなかったこと悔やみます。（機能拡張だけでも今から提案してみようかと思います。）

## まとめ

`Kernel#yield_self`を使ったリファクタリングには以下の効果があると言える:

* 再代入をなくす
* 思考の順序と記述の順序を合わせる

また`yield_self`の背景となるコンビネータ論理と、[上位互換の実装](https://github.com/raganwald-deprecated/homoiconic/blob/409f15fee14627e295f2016fdab9f792a8709e8d/2008-10-30/into.rb)についても上に紹介した通りです。