今日は業務のコードを見ていて、これどういう意味なんだろうと疑問に思った部分について調べたのでそれをまとめていきたいと思います。
バリデーションでの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に関してちんぷんかんぷんでしたが、だいぶ理解できるようになってきました。まだ使いこなすまでは練習が必要だとは思いますが頑張っていきたいと思います!
ここまで読んでいただきありがとうございました!