2020年12月16日
プログラミング
Ruby Enumerableモジュールを理解して繰り返し処理を書く際の選択肢を増やす
新しい技術にチャレンジし続けるpalanのアドベントカレンダーDay16です。
昨日は「【iOS14.3リリース🎉】アプリ内ブラウザでWebAR(Webカメラ)が使用できるようになりました」という記事でした。
はじめに
本日は、Enumerableモジュールの繰り返し処理について書いていきます。
最近社内のslack上で繰り返し処理の書き方について議論されることが何度かあり、その際に自身のEnumerableへの理解がふわっとしてるなと感じ、自分の考えの整理に加えて誰かの役に立てばと今回のテーマを選定しました。
なお、本記事における実行環境は、2020年12月16日時点での最新の安定版である2.7.2を使用し、Ruby 2.7.0 リファレンスマニュアルを参考とさせていただきました。
環境
- Ruby: 2.7.2
- rails: 6.0.3 (一部検証に使用しています)
Enumerableモジュールとは
Enumerableモジュールとは繰り返し処理を行うためのモジュールで、モジュール内のメソッドは全て each を使って定義されています。eachはArrayやHash、RangeなどEnumerableモジュールをインクルードしている各クラスで定義されており、eachを用いてEnumerableモジュールに定義されているメソッドを使うことができるという関係性にあります。
つまりEnumerableモジュールは、eachのみで書くことができる繰り返し処理をより簡潔に書くために提供されている拡張機能と考えることができます。
Enumerableモジュールの中でよく使われるメソッドとして、 map(collect)やselect(find_all, filter)があります。
普段何気なく使っているこれらのメソッドに対して「配列やハッシュに使えるメソッド」くらいの理解だったので、eachとEnumerableモジュールの関係性を考えてみるだけでも学びがありました。
本題に入る前に、ArrayやHashにEnumerableモジュールがインクルードされていることを確認します。(当たり前の前提かもしれませんが…)
Enumerableモジュールがインクルードされているがされていることにより、Array#eachやHash#each を使用してmapやselectが使えるようになっているわけですね。
# Array の場合
Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]
# Hashの場合
Hash.ancestors
=> [Hash, Enumerable, Object, Kernel, BasicObject]
それではEnumerableモジュールのメソッドをいくつか見てみましょう。
Enumerableモジュールのメソッドを使った繰り返し処理
ここからは、社内で議題に上がったメソッドや、私が本記事を書くにあたってリファレンスを改めて読んで勉強した中で、便利そうなものをいくつか抜粋して紹介します。
count
countメソッドはレシーバの要素数を返すメソッドです。
[1,2,3,4].count
=> 4
一見すると、Array#size や Hash#size に要素数を返すだけの珍しくもないメソッドです。
しかし、countの場合はブロックを定義することができ、ブロック内の条件で絞り込んだ要素数を取得することができます。
Enumerableモジュールの繰り返し処理ならではの特徴と言えます。
[1,2,3,4].count{ |i| i > 1 }
=> 3
ただ、Array#sizeやArray#sizeではeachを使った繰り返し処理が行われていないと仮定すると、単に要素数を返すだけならsizeの方が処理速度は早いのかなと思いました。実際の数値は未調査ですが、試してみて別記事にまとめようと考えています。
ActiveRecord_Relationの場合は注意!
ActiveRecord_Relationからsizeやcountを呼び出す際は、挙動に注意が必要です。具体的にはSQLが発行されるケースとそうでないケースがあります。
順番に見てみましょう。
まず初めにActiveRecord_Relationクラスのusersを取得します。
# ユーザを取得
users = User.all
上記で取得した users に対しての挙動をそれぞれのメソッドの挙動を見ていきます。
まず初めに、sizeを使用した場合はSQLが発行されず単に要素数が返ってきます。ActiveRecordのsizeが呼び出されていると思うのですが、基本的にはArray#sizeの挙動と同じと言えそうです。
# SQL発行なし
users.size
=> 14
次にcontを使用した場合を見てみると、SQLを発行した結果の件数を返します。クラス名から直接呼び出すことも可能です。Enumerableモジュールのcountがオーバーライドされ、ActiveRecordのcountが呼び出されているようです。
# SQL発行あり
users.count
(1.4ms) SELECT COUNT(*) FROM `users`
=> 14
# SQL発行あり
User.count
(1.0ms) SELECT COUNT(*) FROM `users`
=> 14
最後に、ブロック付きのcountメソッドを見てみます。こちらはSQLが発行されず要素数の取得ができています。ブロック付きで呼び出した場合は、Enumerableモジュールのcountメソッドが呼び出されていると考えられます。
# SQL発行なし
users.count { |user| user.id == 1 }
=> 1
これらの違いを意識することで、SQLを発行せずオブジェクトの要素数を高速で取得したいケースと、SQLを発行してオブジェクトを再取得した上で要素数を取得したいケースで使い分けることができます。
lengthメソッドについては本記事では言及しませんが、sizeのエイリアスという理解で良いかと思います。
また、countに限らずEnumerableモジュールの他のメソッドにおいても、オーバーライドされることで挙動が変わることがあるので注意は必要です。
each_with_object
each_with_objectは初期値を指定して配列やハッシュを組み立てることができるメソッドです。
[1,2,3,4].each_with_object([]) { |i, a| a << i * 2 if i > 1 }
=> [4, 6, 8]
下記の、map + compactで書くケースと、select + mapで書くケースで書き換えることができます。
map + compactから見ていきます。mapはブロック内のすべての結果を要素とした配列を返すため、そのままでは条件に合致しない要素はnilとして配列に入り込んでしまいます。そのため、条件で絞り込んだ配列を取得するにはcompactでnilを除去する必要があります
[1,2,3,4].map { |i| i * 2 if i > 1 }.compact
=> [4, 6, 8]
select + mapで書くケースです。
これは見たまんまですが、selectで絞り込んでから、mapで配列を加工する方法です。
[1,2,3,4].select { |i| i > 1 }.map { |i| i * 2 }
=> [4, 6, 8]
上記のような簡潔な処理であれば、each_with_objectの恩恵は見えにくく、むしろ、select + map のほうが簡潔に見えます。
each_with_objectは絞り込みや加工の条件が複雑になった際に、有効なメソッドだと考えています。
each_with_object と select + map の2つの選択肢を持っておいて、状況で使い分けるのが良いかと思います。
また、配列からハッシュを組み立てたい場合もeach_with_objectを使うことができます(具体的は使用ケースは思い浮かびません…)
filter_map
最後にruby2.7から追加されたfilter_mapです。
filter_mapはブロックを評価した値のうち、真の値を要素とした配列を返します。つまり、条件で絞り込んだ場合にnilが配列入り込まないため、select + map や each_with_objectの代用として使うことができ、簡潔に書くことができます。
ruby2.7を使っている方はぜひ試してみてください。
[1,2,3,4].filter_map { |i| i * 2 if i > 1 }
=> [4, 6, 8]
まとめ
以上、今回の記事ではEnumerableモジュールとメソッドについてご紹介してきました。
記事を書いてみて自分自身の勉強になり、タイトル通り、繰り返し処理を書く際の選択肢を増やすことができたかなと思っています。
Enumerableモジュールはメソッド数もそれほど多くないので、リファレンスマニュアルを一読してみるのも良いかもしれません。
この記事が何かのお役に立てば幸いです。
Rubyのお仕事に関するご相談
Bageleeの運営会社、palanではRubyに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
0
0
関連記事
2021年12月22日
RDBでセットメニューを表現する方法
2021年12月11日
Railsでツリー構造アプリを作ってみた
2021年12月9日
モデルに書いていたメソッドをPOROに切り出してみた!
2021年12月7日
gem cancancanを使ってみた!
2021年11月15日
CustomCopで命名規則を作ってみた
2021年10月29日
clambyを利用したウイルススキャン
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!