お疲れさまです。システム事業部の坂本です。
普段はRuby on Railsを利用したシステム開発を行っています。
今回はRails開発の中で、以前少しハマってしまった挙動があるため
備忘録を兼ねて皆さんにお伝え出来ればと思います。
■はじめに:Enumerable#any?
とEnumerable#all?
まず、RubyにおけるEnumerable#all?
は、配列に対する条件が
全ての要素が真ならtrue
を、何か一つでも偽になるならfalse
を返すメソッドです。
一方、RubyにおけるEnumerable#any?
は、配列に対する条件が
何か一つでも真ならtrue
を、全て偽になるならfalse
を返すメソッドです。
そのため、以下のような挙動になります。
[1, 7, -3].all? { |n| n > 0 } => false [1, 7, -3].any? { |n| n > 0 } => true [1, 7, 14].all? { |n| n > 0 } => true [1, 7, 14].any? { |n| n > 0 } => true [-1, -7, -14].all? { |n| n > 0 } => false [-1, -7, -14].any? { |n| n > 0 } => false
ここまでは明快なのですが、空の配列に対しては以下の通りになります。
[].all? { |n| n > 0 } => true [].any? { |n| n > 0 } => false
それぞれのメソッドで、true
を返すのかfalse
を返すのかが異なります。
この仕様が原因で、以下のようなソースを書いた際に少しハマってしまいました。
■状況
例えば、以下のようなUser
モデルがあったと仮定します。
class User < ApplicationRecord enum status: { active: '稼働中', withdrawn: '退会済' } # 最近登録したユーザか? # return [Boolean] def new_user? created_at + 1.week > Date.today end end
その状況で、以下のような処理を行いたいと考えました。
User
をとある条件で絞り込んで、全員User#new_user?
がtrueになれば処理A
を実行する。誰か1人でも
User#new_user?
を満たさない場合は、処理B
を実行する。
そして、以下のようなソースを書きました。
class UserDataMaintenanceController class << self def execute ## 省略 # もし、conditionsの条件で検索したUserが全員最近登録したユーザだったら if User.where(conditions).all?(&:new_user?) # 処理A else # 処理B end end end end
■上記が原因でハマってしまったこと
私は当初、UserDataMaintenanceController.execute
を実行した際、
以下の通りに実行されるだろうと考えていました。
【パターン1】もし、conditionsの条件で検索したUserが、全員最近登録したユーザだったら
処理A
を実行する。【パターン2】もし、conditionsの条件で検索したUserに、1人でも最近登録したユーザじゃないユーザがいれば
処理B
を実行する。【パターン3】そもそも、conditionsの条件で検索したUserが誰もいなければ
処理B
を実行する。
しかし、前述でもお伝えした内容からお察しの通り、
パターン3は以下の通りになります。
User.where(...).all?(&:new_user?) # ↓ User.where(...)の結果が、[]だった場合 [].all?(&:new_user?) # => true
そのため、conditionsの条件で検索したUserが誰もいなかった場合に実行されるのは、
当初想定していた処理B
ではなく、処理A
でした。
そして私は当初、この仕様に気づかず数時間程「何故動かないのだろう……?」と頭を悩ませていました……。
暫く調べた後に、前述のEnumerable#any?
とEnumerable#all?
の挙動が違うことに気付き、
以下のように改修することで解決出来ました。
class UserDataMaintenanceController class << self def execute ## 省略 users = User.where(conditions) # もし、conditionsの条件で検索したUserが存在し、全員が最近登録したユーザだったら if users.present? && users.all?(&:new_user?) # 処理A else # 処理B end end end end
こうすると、【パターン3】の「そもそも、conditionsの条件で検索したUserが誰もいなければ処理B
を実行する。」が実現出来ました。
■まとめ
Enumerable#any?
や Enumerable#all?
を使用する際は、
「メソッド実行対象の配列は、どんな時でも必ず要素が1個以上ある配列になるのか?」
を確認した上で使うようにしましょう。
この辺をしっかり確認せずに使うと、
今回お伝えしたような内容でハマってしまう原因になりかねません。
以上、ありがとうございました。