エンジニアの澤田です。
マネーフォワード社内のGitHubプルリクエストに見られるRubyの書き方について、気になったところをもとにして、関係することを連載で考察していきます。
※ 題材とするコードは、社内のGitHubプルリクエストで実際に見かけたコードから問題点に関係する部分を抽出し、抽象化したもので、見かけたものそのままではありません。
初回の今回は、配列の生成について考察します。
プログラム中で使う配列には、他の情報から導かれない一次的なものと、もととなる他の情報を変形させて得られる二次的なものがあります。 前者の場合、次のようにプログラム本体や設定ファイルなどのどこかにリテラルとして書くより他にありません。
[23, 12938, 382]
対して、上の配列が既にどこかに書かれていて、その各要素を2倍にして次の別の配列を作った場合、これは後者の例です。
[46, 25876, 764]
あるいは、次のハッシュがどこかに書かれているとして、
{foo: 19527, bar: 429832, baz: 2}
これをもとにして、(キーでない)各値のうち、偶数であるものだけを並べた次のような配列や
[429832, 2]
これを逆順にした
[2, 429832]
も後者の例です。
後者の場合のように、 Enumerable
なオブジェクトをもとにして、選択、除去、写像、並べ替えなどの加工を施して配列を生成することがよくあります。
これに関して、GitHubプルリクエストで気になった例をもとに紹介します。
選択
(Enumerable
を継承する) Railsの ActiveRecord::Relation
オブジェクト active_record_relation
から一部の要素を選択する次のようなコードを見かけました。
[見かけた書き方]
array = [] active_record_relation.each do |ar| array << ar if some_condition(ar) end
このコードでは、空配列を初期化した後、 active_record_relation
の各要素について、条件を満たせば追加しています。
これは、Rubyのコードとしては原始的であり、Rubyの機能を活かしきっているとは言えません。
以下で、段階を追って改良していきます。
簡単にするため、以下では active_record_relation
の代わりに配列を使って、
array = [] [1, 2, 3, 4, 5].each do |e| array << e if e.odd? end array # => [1, 3, 5]
の改良を考えます。
まず、初期化が配列 [1, 2, 3, 4, 5]
のイテレーションと同列に書かれているのが気になります。
空配列は最終的に欲しいものでなく、欲しい配列を得るための一時的な状態に過ぎないので、できれば、どこか奥深くに隠したいです。
そこで思いつくのは Enumerable#inject
を使うことです。
このメソッドを使えば、初期値の空配列を、イテレーションの要となる inject
メソッドの引数の中に閉じ込め、次の例の array
のようにブロック変数を使って、現在の空間を変数で汚すことなく配列の途中の状態に言及する事ができます。
[1, 2, 3, 4, 5].inject([]) do |array, e| array << e if e.odd? array end
ここで注意しなければならないのはブロック内の最後の array
です。
これがないと、要素 e
が if ...
の条件を満たす場合には、偶然にも <<
メソッドが array
を返すものであるために違いはありませんが、条件を満たさない場合には ... if ...
全体として nil
を返すので、次のイテレーションでブロック変数 array
として nil
が返ってしまい、エラーを起こします。
ちなみに、このメソッドを使うときには、イテレーションごとにブロック変数 array
が前イテレーションのブロックの返り値に置き換わるので、 array
をメソッド <<
で破壊的に変化させることなく、次のように非破壊的な +
を使って書くことも出来ます(ただし、このやり方は途中でたくさんの配列を作るので、効率的によくありません)。
[1, 2, 3, 4, 5].inject([]) do |array, e| e.odd? ? array + [e] : array end
このようにブロックの最後に明示的に配列を書くことをせず、コードを若干短くすることも出来ます。
それには、 Enumerator#each_with_object
または Array#each
などに続く Enumerator#with_object
を使います。
[1, 2, 3, 4, 5].each_with_object([]) do |e, array| array << e if e.odd? end
each_with_object
や with_object
では、ブロック変数の array
に当たるものはイテレーションを超えて同じオブジェクトを表すので、このオブジェクトに対して破壊的な操作が必要な代わりに、ブロックの最後の値を気にする必要がありません。
しかし、これとてRubyの機能を活かしきっていません。
Rubyには、まさにこのようなことをするために Enumerable#select
(やArray#select
など)というメソッドがあるのです。
[1, 2, 3, 4, 5].select do |e| e.odd? end
こう書けば、配列 array
の途中の状態に言及することすら必要ありません。
さらに、このようにイテレーションの要素に対して引数を伴わない述語で条件を評価する場合には、 &
で始まる最後の引数が to_proc
を経由してブロックとして扱われることを使って、次のように書けます。
[1, 2, 3, 4, 5].select(&:odd?)
こうなると、イテレーションの要素に言及する必要すらありません。
除去
同様に、次のようなコードもよく見かけます。
[見かけた書き方]
array = [] active_record_relation.each do |ar| array << ar unless some_condition(ar) end
active_record_relation
を配列で置き換えた例
array = [] [1, 2, 3, 4, 5].each do |e| array << e unless (e % 3).zero? end array # => [1, 2, 4, 5]
を考えます。この場合は、Enumerable#reject
(やArray#reject
など)を使って
[1, 2, 3, 4, 5].reject do |e| (e % 3).zero? end
のように書けます。
ちなみに、この場合には、ブロック内の条件が複雑なので、 &
と to_proc
を使う方法は使えません。
写像
ある Enumerable
はオブジェクトの各要素に変更を加えたものを集めて配列を作るという、次のようなコードもよく見かけます。
[見かけた書き方]
array = [] active_record_relation.each do |ar| array << ar.some_property end
これも、Rubyの機能を活かしきっていません。
まさにこのような場合のために、Rubyには Enumerable#map
(やArray#map
など)があるのです。上のようなコードの場合、次のように書くべきです。
active_record_relation.map do |ar| ar.some_property end
または
active_record_relation.map(&:some_property)
並べ替え
これは空配列を初期化してから要素と足していくという例を普通は見かけません。
このような目的のためにも、Rubyには Enumerable#sort_by
というメソッドがあります。
[1, 2, 3, 4, 5].sort_by do |e| e % 3 end # => [3, 1, 4, 2, 5]
選択と除去
Enumerable
なオブジェクト enumerable
からある条件を満たす要素だけを集めた配列 selected_array
と満たさない要素だけを集めた配列 rejected_array
の両方が必要なときがあります。
このようなときに、まず selected_array
もしくは rejected_array
の片方を作ってから、 enumerable
(に対応する配列)からそれを引いて他方の配列を作るコードを見かけます。
[見かけた書き方]
enumerable # => もとのオブジェクト ... selected_array # => 条件により選択した要素からなる配列 rejected_array = enumerable.to_a - selected_array
配列の例にすると、こんな感じです。
original_array = [1, 2, 3, 4, 5] selected_array = [] original_array.each do |e| selected_array << e if e.odd? end selected_array # => [1, 3, 5] rejected_array = original_array - selected_array # => [2, 4]
このような場合には、上に書いたように
original_array = [1, 2, 3, 4, 5] selected_array = original_array.select(&:odd?) # => [1, 3, 5] rejected_array = enumerable - selected_array # => [2, 4]
と書くことも出来ますが、 Enumerable#partition
を使って
original_array = [1, 2, 3, 4, 5] selected_array, rejected_array = original_array.partition(&:odd?) selected_array # => [1, 3, 5] rejected_array # => [2, 4]
と一気に書くのが良いでしょう。
まとめ
Rubyには今回言及したような、内部イテレーターと呼ばれる便利な機能がいくつもあります。 目的に合わせて、適切なものを使っていきたいものです。
最後に
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【採用サイト】 ■マネーフォワード採用サイト ■Wantedly | マネーフォワード
【マネーフォワードのプロダクト】 ■自動家計簿・資産管理サービス『マネーフォワード ME』 iPhone,iPad Android
■「しら」ずにお金が「たま」る 人生を楽しむ貯金アプリ『しらたま』 iPhone,iPad
■おトクが飛び出すクーポンアプリ『tock pop トックポップ』
■金融商品の比較・申し込みサイト『Money Forward Mall』
■ビジネス向けクラウドサービス『マネーフォワードクラウドシリーズ』 ・バックオフィス業務を効率化『マネーフォワードクラウド』 ・会計ソフト『マネーフォワードクラウド会計』 ・確定申告ソフト『マネーフォワードクラウド確定申告』 ・請求書管理ソフト『マネーフォワードクラウド請求書』 ・給与計算ソフト『マネーフォワードクラウド給与』 ・経費精算ソフト『マネーフォワードクラウド経費』 ・マイナンバー管理ソフト『マネーフォワードクラウドマイナンバー』 ・資金調達サービス『マネーフォワードクラウド資金調達』