🐘

ActiveRecord の scope を lambda とブロックから理解する

2024/01/13に公開

概要

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 で有名な組み込みメソッドだと、 eachmap などがありますが、
あれらはブロックを引数に渡しています。

[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

このコードでは、ActiveRecordscope メソッドに、第2引数として lamnda で作成した Proc オブジェクトが渡されています。

そして scope メソッドではこの Proc オブジェクトを call で実行しているわけですね。

https://github.com/rails/rails/blob/4fb230d2140054f8d0114c2c8dcc659af10a7972/activerecord/lib/active_record/scoping/named.rb#L154

まとめ

本日の内容は以下でした。

  • ブロックは do~end, {~} といった処理のかたまりである
  • ブロックはそのままコードに書くと SyntaxError になる
  • SyntaxError を起こさないようにオブジェクト(Proc オブジェクト)化するものが lambda
  • lambda は -> を使っても書ける
  • ActiveRecord の scope は、第2引数に Proc オブジェクトを渡して、call で実行している

まだ lambda を自由自在に使いこなすイメージが湧いていないのですが、
この辺の issue にヒントがある気がしています。
https://github.com/yochiyochirb/meetups/issues/1049#issuecomment-114948935

Discussion