Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

マネーフォワード社内PRに見られるRubyの書き方について - (1) 配列の生成

エンジニアの澤田です。

マネーフォワード社内の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 です。 これがないと、要素 eif ... の条件を満たす場合には、偶然にも << メソッドが 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_objectwith_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』

くらしの経済メディア『MONEY PLUS』

■ビジネス向けクラウドサービス『マネーフォワードクラウドシリーズ』 ・バックオフィス業務を効率化『マネーフォワードクラウド』会計ソフト『マネーフォワードクラウド会計』確定申告ソフト『マネーフォワードクラウド確定申告』請求書管理ソフト『マネーフォワードクラウド請求書』給与計算ソフト『マネーフォワードクラウド給与』経費精算ソフト『マネーフォワードクラウド経費』マイナンバー管理ソフト『マネーフォワードクラウドマイナンバー』資金調達サービス『マネーフォワードクラウド資金調達』