読者です 読者をやめる 読者になる 読者になる

プログラミング 美徳の不幸

Ruby, Rails, JavaScriptなどのプログラミングまとめ、解説、備忘録。

inject厨

RubyのEnumerableで使えるメソッドでany?とかselectのようにわかりやすいメソッドたちにまぎれて、injectという奇妙なメソッドがある。
僕もRuby始めたてのころは「injectは配列の合計値を求めるときに使う」と覚えていた。

[1,3,5].inject(:+) #=> 9

でもって、いろいろ学んでいくうちにinject=畳み込み演算というのまでわかった。つまり配列の合計値を求めるところでいうと、
①[1,3,5]から1を取り出す(=初期値)
②次のループで3を取り出す。これを引数にして、前の結果に対してブロックを呼び出す。=要するに1に3をたす、そして結果として4を返す
③次のループで5を取り出す。4.+(5)


こうして結果として9を返す。

で、injectの素晴らしさがわかったのは、この間ファインダを作っていた時に某DeNA内定者の友人から「こうすれば綺麗にかけるよ」と教えてもらったときで、それ以来メソッドをチェーンしていく処理で、かつパターン化可能なものはinjectで書けることにびっくりしている。

例えばRailsで検索を作る時。

params[:name] = "田中"
params[:address] ="仙台"
params[:email'] = ""

こういう検索クエリが投げられたとして、普通これをチェーンしていくとcontrollerでこうなる。

def search
  @users = User.scoped
  @users = @users.where(name: params[:name]) if params[:name].present?
  @users = @users.where(address: params[:address]) if params[:address].present?
  @users = @users.where(email: params[:email]) if params[:email].present?
end

こういうコードをいままでイヤというほど見てきたのだけど、これはずばり汚い。まずand検索のものを全部queryとしてまとめる。

params[:query] = { name: "田中", address: "仙台", email: "" }

そして今度はファインダ部分をscopeとしてmodelに押し込める。

class User < ActiveRecord::Base
  scope :and_search_with_query, lambda { |query|
    query.inject(scoped) do |users, (key, value)|
      users = users.where("#{key}".to_sym => value) if value.present?
      users
    end
  end
end

queryのループを回して、前のループの結果を次のループのusersに使用する。初期値はscoped(User.scoped)で、valueが渡っていたらusers = users.where()が実行される。
注意するのは、ブロックの結果が次のループになるので、明示的にusersを最後に評価しないとif value.present?の真偽値が返ってしまう。

usersっていうのがなんか気持ち悪いなー。