ActiveRecord の scope を lambda とブロックから理解する
概要
ActiveRecord の scope について、今までなんとなく理解していました。
今回 Ruby の lambda から勉強し直してみて理解が深まったので、
アウトプットとしてここに残します。
今回の記事で、以下の -> { where(published: true) } の部分が何をしているかわかります。
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
end
環境
ruby '3.1.2'
gem 'rails', '~> 7.0.8'
ブロックとは
ブロックとは、
do
1 + 1
end
や
{
1 + 1
}
のように、do ~ end, もしくは {~} のことをいいます。
ブロックをメソッドに渡す
each や map などの組み込みメソッド
Ruby で有名な組み込みメソッドだと、 each や map などがありますが、
あれらはブロックを引数に渡しています。
[1, 2, 3].each do |i|
result = i + 1
puts "答えは#{ result }です"
end
# 実行結果
答えは2です
答えは3です
答えは4です
=> [1, 2, 3]
[1, 2, 3].each {|i|
result = i + 1
puts "答えは#{ result }です"
}
# 実行結果
答えは2です
答えは3です
答えは4です
=> [1, 2, 3]
自作のメソッド
さきほどは組み込みメソッドの each にブロックを渡して、ブロックの中身を実行しましたね。
では、自作のメソッドで、同じことをするにはどうすればいいでしょうか。
まずは exec メソッドを定義してみましょう。
def exec
end
そしてブロックを渡して実行してみます。
exec do
result = 1 + 1
puts "答えは#{ result }です"
end
#=> nil
exec {
result = 1 + 1
puts "答えは#{ result }です"
}
#=> nil
exec # なにも渡さず実行
#=> nil
どれもブロックの中身は実行されず、 nil が返ってきていますね。
exec メソッドに何も引数を渡さないときと同じ動きになってしまいます。
メソッド内で渡されたブロックを実行するには、yield を使う
ここでよく聞く yield が登場しました。
これは実は、メソッド内で渡されたブロックの中身を実行しているんですね。
def exec
yield
end
exec {
result = 1 + 1
result
}
#=> 2
exec do
result = 1 + 1
result
end
#=> 2
yield を使っていてブロックが渡されない場合、LocalJumpErrorが発生します。
exec
#=> in `exec': no block given (yield) (LocalJumpError)
メソッドに渡される引数がブロックと明示的にしたい場合は、
& を先頭につけて引数を受け取ります。
def exec(&arg)
yield
end
exec do
result = 1 + 1
result
end
#=> 2
さきほどと同じように、渡されたブロックの中身が実行できています。
また、yield ではなく、call メソッドでブロックの中身を実行することも可能です。
def exec(&arg)
arg.call
end
ブロックを持ち運べる形(オブジェクト)にする
ブロックは変数化できない
ブロックをメソッドの引数にできると説明しました。となると、
「ブロックを変数に代入して使いまわしたいなあ...」
と思うはずです。思ってください。
では、実際にブロックを変数に入れてみましょう。
arg_1 = {
1 + 1
}
#=> syntax error, unexpected '\n', expecting => (SyntaxError)
arg_2 = do
1 + 1
end
#=>syntax error, unexpected `do' (SyntaxError)
と、このように SyntaxError を起こしてしまいます。
つまり、Ruby では構文のルール上、ブロックを変数に格納することはできないのです。
ブロックをオブジェクト化するためのものが lambda
Ruby の構文上、変数化できないのがブロックでした。
それを解決するのが lambda になります。
実際に変数に代入してみましょう。
arg_1 = lambda {
1 + 1
}
#=> #<Proc:0x0000ffff9b107368 (irb):529 (lambda)>
arg_2 = lambda do
1 + 1
end
=> #<Proc:0x0000ffff99fcf2a8 (irb):535 (lambda)>
上記の通り、SyntaxError にならず、Proc オブジェクトが作成されていることがわかります。
では lambda で変数化した Proc オブジェクトを、 exec メソッドの引数に渡してみましょう。
このとき、先程のように & は必要ありません。
さきほどはブロックを渡していたので & が必要でしたが、
今回渡すのは Proc オブジェクトであってブロックを期待しているわけではないのです。
def exec(arg)
arg.call
end
arg_1 = lambda {
1 + 1
}
arg_2 = lambda do
1 + 1
end
exec(arg_1)
=> 2
exec(arg_2)
=> 2
このように、問題なく実行できていることがわかります。
lambda でブロック引数を使う
call の引数をブロック引数にわたすこともできます。
def exec(arg)
arg.call(2) # 2がブロック引数 x に渡る。
end
arg_1 = lambda { |x|
x + 1
}
arg_2 = lambda do |x|
x + 1
end
exec(arg_1)
#=> 3
exec(arg_2)
#=> 3
ブロック引数を複数渡すこともできます。
def exec(arg)
arg.call(10, 20, 30)
end
arg_1 = lambda { |x, y, z|
x + y + z
}
arg_2 = lambda do |x, y, z|
x + y + z
end
exec(arg_1)
#=> 60
exec(arg_2)
#=> 60
lambda の別な書き方
lambda は以下のように -> という構文で書くこともできます。
arg_1 = -> {
1 + 1
}
arg_2 = -> do
1 + 1
end
ブロック引数を扱うときは以下のように定義します。
(なぜ || じゃなくなったのかは知らない。)
arg_1 = -> (x, y, z) {
x + y + z
}
arg_2 = -> (x, y, z) do
x + y + z
end
改めて scope をみてみる
さて、一通り lambda の説明をしてみましたが、
最初に見せたコードの理解が少し深まったのではないでしょうか。
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
end
このコードでは、ActiveRecord の scope メソッドに、第2引数として lamnda で作成した Proc オブジェクトが渡されています。
そして scope メソッドではこの Proc オブジェクトを call で実行しているわけですね。
まとめ
本日の内容は以下でした。
- ブロックは
do~end,{~}といった処理のかたまりである - ブロックはそのままコードに書くと SyntaxError になる
- SyntaxError を起こさないようにオブジェクト(Proc オブジェクト)化するものが lambda
- lambda は
->を使っても書ける - ActiveRecord の scope は、第2引数に Proc オブジェクトを渡して、
callで実行している
まだ lambda を自由自在に使いこなすイメージが湧いていないのですが、
この辺の issue にヒントがある気がしています。
Discussion