Ruby on Rails

【Rails】scopeの定義での-> {}について調べてみた

今日は業務のコードを見ていて、これどういう意味なんだろうと疑問に思った部分について調べたのでそれをまとめていきたいと思います。

バリデーションでのifオプションやscopeの定義での -> { }について

バリデーションやscopeの定義のときに以下のように, -> ブロックだったりシンボルが渡されているのは一体何なんだろうと思い調べてみることにしました。

class Book < ApplicationRecord
  belongs_to :publisher, optional: true
  has_one :category, dependent: :destroy
  validates :publisher, presence: true, if: :has_publisher?

  def has_publisher?
    publisher_id.present?
  end

  scope :published?, -> { where(publish_flag: true) }
end

招待はProcオブジェクト

その正体はProcオブジェクトでした。Procオブジェクトの前にブロックが分かる必要があったので、こちらをまず見ていきます。

ブロックについて

ブロック付きでhogeメソッドを呼び出して、hogeメソッド内でyieldを使うと渡したブロックが実行されます。

def hoge
  puts 'hoge'
  yield
end

hoge do
  puts 'huga'
end

# hoge
# huga

ブロックが渡されたかどうかを確認する際には、block_given?メソッドを使います。

def hoge
  puts 'hoge'
  if block_given?
    yield
  end
end

hoge
# hoge

hoge do
  puts 'huga'
end
# hoge
# huga

yeildはブロックに引数を渡すこともできます。

def boo
  puts 'boo'
  text = yield 'fuga'
  puts text
end

boo do |text|
  text * 2
end

# boo
# fugafuga

ブロックをメソッドの引数として明示的に受け取ることもできます。その場合、引数の前に&をつけます。

def foo(&block)
  puts 'foo'
  text = block.call('呼び出し成功!')
  puts text
end

foo do |text|
  text * 2
end

# foo
# 呼び出し成功!呼び出し成功!

Procオブジェクトとは

Procクラスはブロックをオブジェクト化するためのクラスです。Procオブジェクトはつぎのようにして作ります。

hello_proc = Proc.new { puts 'hello' }
hello_proc.call
# hello

add_proc = proc { |a, b| a + b }
num = add_proc.call(1, 2)
puts num
# 3

ブロックの説明でブロックを引数として明示的に受け取ることができると書きました。これは実はProcクラスのオブジェクトです。確認してみましょう。

def foo(&block)
  puts 'foo'
  puts block.class # 追記
  text = block.call('呼び出し成功!')
  puts text
end

foo do |text|
  text * 2
end

# Proc
# 呼び出し成功!呼び出し成功!

つまり、ブロックの代わりにProcオブジェクトを引数で渡すこともできます。しかし、Procオブジェクトをブロックとして渡す必要があり、その場合&をつけます。

def foo(&block)
  puts 'foo'
  text = block.call('呼び出し成功!')
  puts text
end

proc = Proc.new { |text| text * 2 }
foo(&proc)
# foo
# 呼び出し成功!呼び出し成功!

&の役割はそれだけではなく右辺のオブジェクトに対してto_procメソッドを呼び出し、その戻り値として得られたProcオブジェクトをブロックを利用するメソッドに与えます。to_procメソッドはProcオブジェクトに対して呼び出しても何も変化しませんが、シンボルに対して呼び出すとProcオブジェクトをつくります。

split_proc = :split.to_proc
=> #<Proc:0x00007fdb3c064bf8(&:split)>

Procオブジェクトに引数を渡して実行すると、1つ目の引数をレシーバにし、そのレシーバに対して元のシンボルと同じ名前のメソッドを呼び出します。

split_proc.call('a-b-c-d e')
=> ["a-b-c-d", "e"]

引数が2つあると、1つ目の引数はレシーバで、2つ目の引数はsplitメソッドの第一引数として実行されます。

split_proc.call('a-b-c-d e', '-')
=> ["a", "b", "c", "d e"]

これらのことを踏まえるとブロックの代わりに&シンボルを渡す下のメソッドの挙動を説明することができます。

['apple', 'banana', 'melon'].map(&:upcase)
=> ["APPLE", "BANANA", "MELON"]
  • &:upcaseはシンボルの:upcaseに対してto_procメソッドを呼び出し、Procオブジェクトとする。
  • Procオブジェクトはmapメソッドから各要素を第一引数として受け取る。第一引数はupcaseメソッドのレシーバとなる。
  • mapメソッドはProcオブジェクトの戻り値を新しい配列に詰め込む。

補足

Procオブジェクトを普通の引数として受け取る際には、&をつけません。

def foo(proc)
  text = proc.call('呼び出し成功!')
  puts text
end

proc = Proc.new { |text| text * 2 }
foo(proc)
# 呼び出し成功!呼び出し成功!

ラムダ記法によるProcオブジェクトの作成

Procオブジェクトを作る方法は他にもあり、->構文、またはlambdaメソッドで作ることができます。だんだんscopeの書き方に似てきましたね。

->(a, b) { a + b }
lambda { |a, b| a + b }

->構文、またはlambdaメソッドで作られるProcオブジェクトはラムダと呼ばれ、普通のProcオブジェクトとは少し違いがあります。

add_proc = Proc.new { |a, b| a.to_i + b.to_i }
puts add_proc.call(1)
puts add_proc.call(1, 2, 3)
# 1
# 3

add_lambda = -> (a, b) { a.to_i + b.to_i }
puts add_lambda.call(1)
# wrong number of arguments (given 1, expected 2) (ArgumentError)

ここまでで来るともう最初に感じた疑問点は解決できそうです。

if: :has_publisher?や-> { where(publisher_flag: true) }はなにしてる?

シンボルや-> {}が来ている時点で察しがつくとは思いますが、これらは上で書いたことがふんだんに使われています。

まず、if: :has_publisherですが、内部で#to_procが使われていて、procオブジェクトにしていると思われます。(ソースコードは見ていないので当てずっぽうです) ここはラムダでも動作しました。

class Book < ApplicationRecord
  # 略
  validates :publisher, presence: true, if: :has_publisher?

  def has_publisher?
    publisher_id.present?
  end
end

# コンソール
book = Book.new(name: '本のタイトル', publisher_id: 1000)
book.valid?
=> false
book.errors.full_messages
=> ["Publisher can't be blank"]
class Book < ApplicationRecord
  # 略
  validates :publisher, presence: true, if: -> { publisher_id.present? }
end

# コンソール
book = Book.new(name: '本のタイトル', publisher_id: 1000)
book.valid?
=> false
book.errors.full_messages
=> ["Publisher can't be blank"]

一方、scopeですが、こちらはメソッドを定義していて第一引数にメソッド名、第二引数に実行するProcオブジェクト(ラムダ)を定義しています。なので、当然Proc.newを持ってきても動作します。

class Book < ApplicationRecord
  # 略
  scope :published, -> { where(publisher_flag: true) }
  scope :published, lambda { where(publisher_flag: true) }
  scope :published, Proc.new { where(publisher_flag: true) }
  scope :published, proc { where(publisher_flag: true) }
end

# コンソール
Book.published
=> [#<Book:0x00007f80f8bccf20
  id: 91,
  #略

もぐくん
もぐくん
ずっとおまじないのように, -> {}を使ってたよ

まとめ

ここまで、ずっと疑問に思っていたscopeなどの挙動が理解できてよかったです。初めてチェリー本を読んだときProcに関してちんぷんかんぷんでしたが、だいぶ理解できるようになってきました。まだ使いこなすまでは練習が必要だとは思いますが頑張っていきたいと思います!

ここまで読んでいただきありがとうございました!

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!