マネーフォワード社内PRに見られるRubyの書き方について – (2) ハッシュの生成

エンジニアの澤田です。

この連載では、社内のRuby (on Rails)コードで気になった箇所の問題点やそこから発展して関連事項を議論しています。
1回目の 社内PRに見られるRubyの書き方について (1) では配列の生成を扱いましたが、今回はハッシュ(Hash)の生成を考察します。

題材とするコードは、社内のGitHubプルリクエストで実際に見かけたコードから問題点に関係する部分を抽出し、抽象化したもので、見かけたものそのままではありません。
また、本稿で述べるオブジェクトの分類や用法は筆者独自の見解であることをご了承下さい。


ハッシュは配列に似ている面があります。配列では「要素」、ハッシュでは「値」と呼ばれる、(実用上)任意の個数のオブジェクトの集合を蓄えるという点です。一方で、ハッシュは配列よりも複雑な情報を持ち、多様な使い方があるため、その生成を考えるに当たっては、配列ではあまり意識しなかった用法の違いを考慮することが有効です。以下で、オブジェクトの情報の複雑さという観点からオブジェクトを分類し、配列やハッシュの主な用法を考えます。

オブジェクトに蓄えられるオブジェクトの系列

配列は要素の集合、ハッシュは値の集合を持っていますが、ハッシュは他に「キー」と呼ばれるオブジェクトの集合も持っています。1つのハッシュのキーの全体は一般には不均質な集まりであり、キーは個別に特徴付けられるため、キーの情報はハッシュが1つずつ保持しておかなければならない情報です。本稿では、ある配列の要素の全体やあるハッシュの値の全体に相当する情報を「主系列」、あるハッシュのキーの全体に相当する情報を「副系列」と呼ぶことにします。

巷では、ハッシュのキーには配列の「インデックス」が相当すると考えることもあるようですが、本稿ではそう考えません。まず、ハッシュもインデックスを持っていて、Enumerable#each_with_indexにより呼び出すことが出来ます。そして、配列やハッシュの持っているインデックスはハッシュのキーとは性質が異なります。1つの配列やハッシュのインデックスの全体は均質な集合であり、個別のインデックスには特徴がありません。インデックスは配列やハッシュが1つずつ保存しておく必要のある情報ではありません。実際、1つの配列の(重複を区別した)要素の全体や1つのハッシュのキーの全体もしくは(重複を区別した)値の全体の全順序が与えられれば、個別のインデックスは演繹される情報であり、0から始まる連続した整数を当てるというのはその関係を表現する1つの恣意的な方法に過ぎません。(ちなみに、ここではこれらのクラスがRubyインタープリタの内部でどう実装されているかを問題にせず、オブジェクトの持つ情報という観点で考えています。)

逆に、区間(Range)オブジェクトは、インデックスを持ちながら、オブジェクトの集合の系列を持ちません。区間は開始と終了を表すオブジェクトをそれぞれ持っていますが、その中間の値について、個別の情報を持っていません。

ややこしい場合として、Structのサブクラスがあります。Structのサブクラスは、主系列の情報(つまり値)だけでインスタンスを生成することが出来ます。2つ目の系列(つまりキー)はサブクラスの定義時に与えられていて、インスタンスごとに直接には保持していません。それでも、どのサブクラスに属しているかというのはインスタンス固有の情報であり、そのサブクラスが2つ目の系列の情報を持っているので、インスタンスも(間接的に)副系列を持っていると考えることにします。

このように、オブジェクトは、その蓄えるオブジェクトの系列の数によって分類できます。本稿では、系列を持たないオブジェクトを「0系列オブジェクト」、主系列を持ち副系列を持たないオブジェクトを「1系列オブジェクト」、主系列と副系列を持つオブジェクトを「2系列オブジェクト」と呼ぶことにします。0系列オブジェクトには、区間インスタンスや等差数列(Enumerator::ArithmeticSequence)インスタンスなどがあり、1系列オブジェクトには、配列インスタンスやSetインスタンスなどがあり、2系列オブジェクトには、ハッシュインスタンスや、ENVOpenStructのインスタンス、Structのサブクラスのインスタンス、Ruby on RailsのActionController::Parametersインスタンスなどがあります。

配列やハッシュの主な用法

配列には副系列がないため、特定の個別の要素を特徴付けず、主系列が人の名前の集合とか、数字の集合とか、同じキー構成を持つハッシュの集合というように、「均質」な集合である事が多いです。ここで均質とは、オブジェクトのクラスが同じだけでなく、その上にどれも人の名前を表すとか、どれも住所だというように、同じものだけの集まりを指すものとします。

["佐藤", "山田", "鈴木", "高橋"]
[1, 1, 2, 3, 5, 8, 13]
[
  {"日" => "2018-12-31", "起床" => "09:17", "昼食" => "ハンバーガー"},
  {"日" => "2019-1-1", "起床" => "08:01", "昼食" => "そば"},
  {"日" => "2019-1-2", "起床" => "09:25", "昼食" => "サンドイッチ"},
  {"日" => "2019-1-3", "起床" => "08:38", "昼食" => "うどん"},
  {"日" => "2019-1-4", "起床" => "08:42", "昼食" => "サラダ"},
]

ハッシュの主系列も、このような均質な集合を表すのによく使われます。

{"ボーカル" => "佐藤", "ギター" => "山田", "ベース" => "鈴木", "ドラムス" => "高橋"}
{1 => 1, 2 => 1, 3 => 2, 4 => 3, 5 => 5, 6 => 8, 7 => 13}
{
  "月曜日" => {"日" => "2018-12-31", "起床" => "09:17", "昼食" => "ハンバーガー"},
  "火曜日" => {"日" => "2019-1-1", "起床" => "08:01", "昼食" => "そば"},
  "水曜日" => {"日" => "2019-1-2", "起床" => "09:25", "昼食" => "サンドイッチ"},
  "木曜日" => {"日" => "2019-1-3", "起床" => "08:38", "昼食" => "うどん"},
  "金曜日" => {"日" => "2019-1-4", "起床" => "08:42", "昼食" => "サラダ"},
}

ところが、ハッシュは副系列を持つため、個別の値をそれぞれ特徴付けることが出来、主系列が不均質になる使われ方も多いです。そのような中で、特に副系列がプログラムの実行中は固定されているものが多く使われます(ちなみに、均質な主系列を持つハッシュにも、副系列が固定された場合とそうでない場合がありますが、その区別は本稿では問題にしません)。例えば、人の名前と住所と年齢とあるサービスへの入会の有無というように多様な情報を持つハッシュの形式があり、そのようなハッシュが一旦目的の形に生成されたら、その後、プログラムの実行中に、その個別の値が書き換えられたり、ハッシュ自体が削除されたりするけれども、キーが変更されない(プログラムの改修時には変更されるかも知れない)というような場合がこれに当たります。

{name: {last: "田中", first: "太郎", middle: nil}, address: "東京都港区芝浦", age: 20, membership: false}

データ構造の用語でいうと、均質な主系列を持つ配列やハッシュは「リスト」、不均質な主系列と固定された副系列を持つハッシュは「構造体」におおよそ対応すると思いますが、これらの用語はある特定のプログラミング言語での具体的な型や実装方法に言及することもあるのに対して、ここでは配列(Array)やハッシュ(Hash)というRubyの具体的なクラスに対してのそれらの用法を問題にしたいので、混乱を避けるため、本稿ではこれらの用語を使いません。

関係データベースのモデルである関係モデルとの対応で(個別の情報がデータベースで表せるクラスに属するどうかや、第1正規形を満たしているかという問題を考えずに)この違いを捉えると、均質な主系列を持つ配列やハッシュでは、主系列はデータベースの表の1つの列、即ち関係モデルの1つの属性に相当します。さらにハッシュの場合には、副系列は関係モデルの1つの候補キーに相当します。

担当 名前
ボーカル 佐藤
ギター 山田
ベース 鈴木
ドラムス 高橋

不均質な主系列と固定された副系列を持つハッシュの場合には、主系列はデータベースの表の1つの行、即ち関係モデルの1つの組に相当し、副系列は関係モデルの見出しに相当します。

ID name address age membership
1 田中 太郎 東京都港区芝浦 20 false

つまり、配列は関係モデルの属性に対応させて考えられることが多く、ハッシュは関係モデルの候補キーと属性に対応させて考えられることもあれば、見出しと組に対応させて考えられることもあります。

配列やハッシュのこのような用法の違いを踏まえた上で、次節でハッシュの生成を考えます。

リテラルによるハッシュの生成

あるオブジェクトを変形させて得られないような配列やハッシュを書き表すには、リテラルを使う以外になく、それについてそれ以上ここで述べることはありません。一方、あるオブジェクトの持つ情報を加工して配列やハッシュを生成する場合は、配列やハッシュの用法によって事情が異なります。

他のオブジェクトに基いて主系列が均質な配列やハッシュを生成する場合、加工に要する操作はどの要素や値についても同じになり、従って、もとになるオブジェクトから写像などの操作により生成することが出来ます。例えば、もとになるオブジェクト:

array = [
  {"日" => "2018-12-31", "起床" => "09:17", "昼食" => "ハンバーガー"},
  {"日" => "2019-1-1", "起床" => "08:01", "昼食" => "そば"},
  {"日" => "2019-1-2", "起床" => "09:25", "昼食" => "サンドイッチ"},
  {"日" => "2019-1-3", "起床" => "08:38", "昼食" => "うどん"},
  {"日" => "2019-1-4", "起床" => "08:42", "昼食" => "サラダ"},
]

の各要素をhとして、どのhに対してもh["起床"] > "09:00"という一貫した変換を行うことにより、arrayの変形として配列:

array.map{|h| h["起床"] > "09:00"}
# => [true, false, true, false, false]

を生成できます。同様に、主系列が均質なハッシュも、arrayにある操作を行って次のように生成できます。

array.to_h{|h| [h["日"], h["起床"] > "09:00" ? "遅刻" : "早着"]}
# => {"2018-12-31" => "遅刻", "2019-1-1" => "早着", "2019-1-2" => "遅刻", "2019-1-3" => "早着", "2019-1-4" => "早着"}

このような他のオブジェクトを加工して主系列が均質なハッシュを生成することについては後の節で考察します。

一方、他のオブジェクトをもとにして主系列が不均質なハッシュを生成したい場合、各値ごとに異なる論理で選択や、除去、整形などの加工をする必要があり、上のような一貫した操作により生成できないことが多いです。ハッシュの生成では、主系列が不均質な場合、専ら1つのオブジェクトの持つ情報を加工して生成する場合であっても、リテラルを書かなければならないことがよくあります。このことが配列リテラルと比較してハッシュリテラルを複雑にしています。

例として、社内コードのRuby on Railsのコントローラー内で、 params (ユーザーからのリクエストに含まれるパラメーターを含んだActionController::Parametersインスタンス)を使う次のようなパターンが繰り返し出てきます。

[見かけた書き方]
hash = {}
hash[:param_1] = params[:param_1].some_method_1 if params[:param_1]
hash[:param_2] = params[:param_2].some_method_2 if params[:param_2]
...
hash[:param_k] = params[:param_k].some_method_k unless params[:param_k].nil?
...
hash[:param_n] = params[:param_n].some_method_n if params[:param_n]

ここではRuby on Railsに依存しない議論にするために、 params は以下のようなハッシュだと考えてください。

params = {
  param_1: "foo",
  param_k: true,
  param_n: 1,
}

このコードでは、空ハッシュhashを初期化し、それに対して1つずつ条件が合えば値を入れていっています。hashの操作に使われているキーの一覧:param_1, :param_2, …, :param_k, …, :param_nは(必ずしも)paramsのキーの一覧:param_1, :param_k, :param_nと一致していません。paramsのキーに随意的なものがあるのです。

params[:param_1] などをそのままparamsの値に使うのではなく、メソッド some_method_1 などで整形しています。こういう整形用のメソッドは、キーごとに主に想定されるクラスのインスタンス(≒ nilでないインスタンス)に定義されていて、普通はその定義がnilに及びません。そこで、params[:param_1] などがnilになる場合を考えて、メソッド未定義エラーを防ぐために、if params[:param_1] などのコード実行の条件を付けています。一部のパラメーターparams[:param_k]の場合には、nilの他にtrueまたはfalseの値が想定され、if params[:param_k]という条件では、falseの場合を誤って無視してしまうので、特にunless params[:param_k].nil?としています。

以下でこのコードについて議論します。

命令型ステップの回避

連載1回目で問題とした配列の例と同様、このコードは、目的のハッシュとは違う値にhashを初期化し、不必要な手順を踏んでいます。もしかしたら、if ...unless ...という制御構造を使うので、命令型プログラミング的にステップを踏んで書かなければならないというような気持ちに書いた人が囚われてしまったのかも知れません。そのような感性に合理性はあるのでしょうか。実は、Rubyではこれらの制御構造には返り値があります。従って、表現の一部に使うことが出来ます。目的のhashを始めからハッシュリテラルとして次のように書けば一発で出来、また読みやすくもあるでしょう。

hash = {
  param_1: params[:param_1].some_method_1 if params[:param_1],
  param_2: params[:param_2].some_method_2 if params[:param_2],
  ...,
  param_k: params[:param_k].some_method_k unless params[:param_k].nil?,
  ...,
  param_n: params[:param_n].some_method_n if params[:param_n],
}

if ...unless ...構文は、1つ目のコードの中では、返り値が問題にならない命令として使われていたのに対して、2つ目のコードでは、返り値が問題となる関数として使われています。

ただし、2つ目のやり方では、paramsにない:param_2などに対応する値も(nilとして)hashに書かれます。そして、hash[:param_2]が呼び出されたときの返り値はnilとなり、hash:param_2などに対応する値がそもそも書かれなかった場合と似た振る舞いをするようになります。それで問題がなければ、ここまでのところ、これでよいでしょう。問題になる場合の対応については、後の節で述べます。

それでもif ...unless ...という制御構造を関数的に使うことにやはり違和感を感じるという人もいるかも知れません。そういう人は、論理演算子&&などを使って

hash = {
  param_1: params[:param_1] && params[:param_1].some_method_1,
  param_2: params[:param_2] && params[:param_2].some_method_2,
  ...,
  param_k: params[:param_k].nil? ? nil : params[:param_k].some_method_k,
  ...,
  param_n: params[:param_n] && params[:param_n].some_method_n,
}

と書いてみることも出来ます。三項演算子... ? ... : ...は意味的に見れば制御構造ですが、名前にも現れているように、演算子的に使われることが想定されているので、上のように使うことに違和感はないでしょう。

:param_kに対応する値の部分は

(!params[:param_k].nil? || nil) && params[:param_k].some_method_k

とすれば、三項演算子を使わずに済み、また他のパラメーター値を与えるコードの部分との違いを縮小できます。... || nilfalsenilに変換する働きをします。!params[:param_k].nil? || nilで、params[:param_k]を「nil以外対nil」に場合分けし、「truenil」に変換しています。でもこれはあまりエレガントではありませんね。それは次の節で解決します。

制御構造if p then q end, unless p then q end, p ? q_1 : q_2, case r when p then q end, while p do q end, until p do q endなどを使った場合には、後件部q, q_1, q_2が構文の意味上評価されないことで未定義エラーを回避しているのに対して、論理演算子&&, ||, and, orなどを使った場合には、短絡評価により未定義エラーを回避しているという思想の違いがありますが、結果は同じです。

ところで、これらコード評価の回避に使える表現に登場するトークン(if, unless, ?, case, while, until, &&, ||, and, orなど)の全てに共通することがあります。それは、どれもメソッドではないということです。これにはRubyのメソッド評価の仕方が関係しています。

定式化してみましょう。ローカル変数pPというクラスのインスタンスかもしくはnilを表す可能性があり、qPのインスタンスメソッドではあるが、nilに対しては定義されていないとします。このとき、次のように、p.qをレシーバー、pを引数として取り、pnilである場合にp.qの評価を回避するメソッドif:

p.q.if(p)

も、次のようにpをレシーバー、p.qを引数として取り、pnilである場合にp.qの評価を回避するメソッドand:

p.and(p.q)

も存在しません。

背理法で示します。命題に反して、上のようなif, andメソッドがあるとします。Rubyがメソッド評価をするとき、まずレシーバーを評価し、次に引数を評価し、その後にそれらをメソッドに引き渡して、メソッドの評価をします。ifメソッドの場合もandメソッドの場合も、メソッドが評価され始めたときは、p及びp.qが評価された後です。従って、ifandがその内部でいかなることを行おうとも、p.qの評価を止めることは不可能です。■

条件内の重複の回避

上の&&を使ったコードまで来ると、params[:param_1]などが近い場所で繰り返されているのが気になってきます。そこで使えるのが、一般的に「Null条件演算子」(safe navigation operator)、あるいはRubyコミュニティーからは「ぼっち演算子」(lonely operator)と呼ばれている&.です。これを使って、

hash = {
  param_1: params[:param_1]&.some_method_1,
  param_2: params[:param_2]&.some_method_2,
  ...,
  param_k: params[:param_k]&.some_method_k,
  ...,
  param_n: params[:param_n]&.some_method_n,
}

というようにparams[:param_1]などの繰り返しをなくせます。

また、falseになり得る:param_kに対する値の書き方が、他のキーに対するものと同様になり、例外的でなくなったことにも注目してください。前節の終わりで:param_kの値に対する書き方に困難が生じたのは、評価回避の有無をparams[:param_k]が「nilかそれ以外か」で場合分けしたいのに対して、制御構造や論理演算子の場合分けが「nilもしくはfalseかそれ以外か」に基づいているというずれがあるからです。一方&.は、「nilかそれ以外か」で場合分けして評価回避を決めます。次のコード

foo&.bar(baz)

foonilの場合には 、引数bazやメソッドbarの評価を回避し、nilを返します。それ以外の(foofalseなどの)場合にはfoo.bar(baz)を評価します。上のhashのコードでは、params[:param_k]falseであっても、メソッドsome_method_kに引き渡されて整形されます。このことは、&.の仕様のデザインで「nilかそれ以外か」を選び、「nilもしくはfalseかそれ以外か」としなかったことが正しかったと考える一つの根拠になります。

メソッドの返り値としてのハッシュの生成

前節では、不均質な主系列と固定された副系列を持つハッシュをリテラルで生成することを扱いましたが、この節では情報のもととなるオブジェクトに対してメソッドを適用して返り値としてハッシュを生成することを扱います。この領域のRubyのハッシュの機能はこれまで配列ほどには充実していませんでした。しかし、近年、いくつかメソッドが追加され、便利になってきました。先日リリースされたRuby 2.6にもまた、ハッシュに関わる機能が追加されています。この節では、そうした最近導入された機能を紹介します。

前述したとおり、主系列が均質なハッシュは、メソッドの返り値として生成できるものが多いです。一方、主系列が不均質なハッシュの生成では、専ら1つのオブジェクトの持つ情報を加工して生成する場合であっても、リテラルを書かなければならないことが多いと書きましたが、限られた種類の加工については、メソッドの返り値として得られることもあります。

追加

既存のハッシュold_hashに複数の値を追加もしくは変更するときに、次のようなコードを社内で良く見かけます。

[見かけた書き方]
hash = old_hash.dup
hash["foo"] = "FOO"
hash["bar"] = "BAR"
hash["baz"] = "BAZ"

ここで、old_hashは目的のhashとは別に残しておくために、dupにより同じ内容のハッシュをまず作っています。

このコードは、1つずつキーと値を与える手順を書いていて、前節のリテラルによるハッシュの生成で見かけたコードに似ています。この場合には、命令型プログラミングを避けるためにHash#mergeを使って書くのが良いです。

hash = old_hash.merge(
  "foo" => "FOO",
  "bar" => "BAR",
  "baz" => "BAZ",
)

除去

ハッシュから引数で指定した値と対応するキーを除去してハッシュを生成する操作は、あまり需要がないようで、そういう操作をするメソッドはありません。

ただし、固定された値nilを取り除くメソッドHash#compactがあります。デフォルト値が設定されていないハッシュでは、あるキーについてnil値を登録してもしなくても、そのキーによる値の呼び出しの返り値は同じnilになります。そのような2つの場合、即ち登録された値のnilが返されることと登録されていない状態でnilが返されることの混在を避けるために、登録されているnilを除くという用途に使います。

このメソッドは、筆者がRuby開発者の方にお願いしてRuby 2.4から実装してもらいました。1 Hash#compactHash#compact!は、古くからあるArray#compactArray#compact!から類推したもので、Array#compactなどがnilである要素を除くのと同様に、Hash#compactなどは、 nil な値と対応するキーを除きます。Ruby on Railsでも同じ機能が実装されてきましたが、Ruby内部で実装され、フレームワークに関わりなく使えるようになったことに意義があります。

主系列が均質な場合で気になった次のような例があります。

[見かけた書き方]
hash = {}
old_hash.each {|k, v| hash[k] = v if v}

ここでold_hashnilな値を含んだハッシュです。この場合、

hash = old_hash.compact

と簡単に書けます。

また、compactは主系列が不均質な場合にも有効です。前節で問題にしたコードで生成したハッシュは主系列が不均質ですが、主系列が多様なオブジェクトになる可能性がありながらも、それらがnilという共通のオブジェクトを値の不在という共通の意味を表すために使っています。そのために、主系列が不均質ありながらもnilを除去するという共通の操作を適用することが出来ます。次のように、compactをハッシュリテラルの後に呼ぶだけで、params[:param_2]nilの場合には、hash:param_2に対する値を含まなくなります。

hash = {
  param_1: params[:param_1]&.some_method_1,
  param_2: params[:param_2]&.some_method_2,
  ...,
  param_n: params[:param_n]&.some_method_n,
}.compact

一方、引数で指定したキーと対応する値をハッシュから除去する操作は、非破壊的なメソッドはありませんが、破壊的であれば、Hash#deleteがあります。

hash.delete(:foo) # => "foo"
hash # => {:bar => "bar", :baz => "baz"}
hash.delete(:qux) # => nil
hash # => {:bar => "bar", :baz => "baz"}

ENVからも同様の特異メソッドENV.deleteを使ってハッシュを作ることが出来ます。

ENV # => {"FOO_KEY" => "foo key", "BAR_KEY" => "bar key", ..., "BAZ_KEY" => "baz key"}
ENV.delete("FOO_KEY") # => "foo key"
ENV #  => {"BAR_KEY" => "bar key", ..., "BAZ_KEY" => "baz key"}
ENV.delete("QUX") # => nil
ENV #  => {"BAR_KEY" => "bar key", ..., "BAZ_KEY" => "baz key"}

この他に、引数でなくブロックで条件を書いてハッシュやENVから削除する操作としてHash#rejectENV.rejectがあります。

選択

ハッシュからのキーによる選択は、除去よりも需要が多いようで、Ruby 2.5から導入されたHash#slice メソッドが使えます。これはRuby on Railsにもともとあって、Rubyに取り入れられたものです。

hash = {foo: "foo", bar: "bar", baz: "baz"}
hash.slice(:bar, :baz) # => {:bar => "bar", :baz => "baz"}

均質な主系列を持つハッシュでは特定のキーに対応する値だけを抜き出すのに使えるでしょう。不均質な主系列を持つハッシュでは必要なキーだけを選別して、もとのオブジェクトから雑音を落とすのに使えます。例えば、以下のようなコードを良く見かけます。

[見かけた書き方]
hash = {}
hash[:foo] = old_hash[:foo]
hash[:baz] = old_hash[:baz]

これはsliceを使って単に

hash = old_hash.slice(:foo, :baz)

と書けば済みます。

ENVについても、Ruby2.6から導入された同様の特異メソッドENV.sliceが使えます。 これは、筆者がRuby開発者の方にお願いして、導入してもらいました。2 ENVは不均質な主系列と固定された副系列を持つオブジェクトの典型例であり、コードの特定の箇所ではそのうちの一部だけが必要であることが多いです。sliceによって不必要なものを削ぎ落とすことが出来ます。

ENV.slice("HOME", "PATH")
# => {"HOME" => "/Users/...", "PATH" => "/Users/foo/bin:/usr/local/bin"}

また、ハッシュから引数でなくブロックによる選択をするには、Hash#selectがあります。

並べ替え

ハッシュのキーや値の順序が重要になるときがあります。例えば、ハッシュを表に変換して表示するときです。インデックス順にハッシュのキーと値を表の行として表示することにすると、順序が見栄えに影響します。そのようなときには、ハッシュを並べ替える必要があります。

均質な主系列を持つハッシュを考えましょう。

hash = {"A社" => "100円", "B社" => "120円", "C社" => "80円"}

このhashを各値に対してto_iを適用した結果によって並べ替えたハッシュが欲しいとします。これを行う単一のメソッドは今のところありません。最も単純なのはsort_byで一旦配列にしてからto_hでハッシュに変換することです。

hash.sort_by{|k, v| v.to_i}.to_h
# => {"C社" => "80円", "A社" => "100円", "B社" => "120円"}

これはプログラマに取っては大した手間ではありませんが、途中ですぐに捨てる配列を生成するのが無駄です。このメソッド連鎖を1つのメソッドで行う機能は、今後どなたかがRuby開発者に提案する余地があるかも知れません。

不均質な主系列と固定された副系列を持つハッシュを並べ替えるときは、キーの指定によることが多いでしょう。例えば、

hash = {"年齢" => 18, "登録" => false, "名前" => "田中"}

"名前", "年齢", "登録"のキーの順に並べ替えたいとします。このときに使えるのが上の選択の節でも言及したsliceです。sliceでは引数のキーの順序が返り値に反映されるので、並べたいキーの順に引き数を与えるだけです。

hash.slice("名前", "年齢", "登録")
# => {"名前" => "田中", "年齢" => 18, "登録" => false}

ENV.sliceでも同様に与えた引数の順序が返り値のハッシュで保持されます。ENVの属性を"HOME", "PATH"の順でなく"PATH", "HOME"の順に得るには、その順に引数を渡すだけです。

ENV.slice("PATH", "HOME")
# => {"PATH" => "/Users/foo/bin:/usr/local/bin", "HOME" => "/Users/..."}

写像

均質な主系列を持つハッシュold_hashがあったときに次のようにハッシュhashを生成するコードを見かけました。

[見かけた書き方]
old_hash = {"foo_1" => object_1, "foo_2" => object_2, ..., "foo_n" => object_n}
hash = {}
old_hash.each {|k, v| hash[k] = v.some_method}

ここではold_hashの各値vを写像v.some_methodによって移しています。命令型プログラミングを避けるとすれば、次のようになります。

hash = old_hash.each_with_object({}){|(k, v), h| h[k] = v.some_method}

どちらの書き方にしても、ハッシュの値を変えたいだけなのに、ブロック内でハッシュやキーに言及しなければならないのがエレガントでない気がします。このような場合はRuby 2.4で導入されたHash#transform_valuesが使えます。

hash = old_hash.transform_values(&:some_method)

このtransform_valuesは特にEnumerable#group_byの直後での需要が高いです。group_byでは、返り値のハッシュのキーをイテレーションされているオブジェクトから合成しますが、そのハッシュの値である配列にはイテレーションされているオブジェクトがそのまま入ります。そのオブジェクトはキーを作るのに十分な情報も含んでいるので、情報に重複があり、いつもというわけではありませんが、この重複を除きたいときがよくあります。例えば

hash = {
  "2018-12-31" => "09:17",
  "2019-1-1" => "08:01",
  "2019-1-2" => "09:25",
  "2019-1-3" => "08:38",
  "2019-1-4" => "08:42",
}

の各値をvとして、v > "09:00" ? "遅刻" : "早着"の評価の結果によってキーを分類して

{
  "遅刻" => ["2018-12-31", "2019-1-2"],
  "早着" => ["2019-1-1", "2019-1-3", "2019-1-4"],
}

を得たいとします。group_byを適用しただけだと

hash
.group_by{|_, v| v > "09:00" ? "遅刻" : "早着"}
# =>
# {
#   "遅刻" => [["2018-12-31", "09:17"], ["2019-1-2", "09:25"]],
#   "早着" => [["2019-1-1", "08:01"], ["2019-1-3", "08:38"], ["2019-1-4", "08:42"]]
# }

となって、ハッシュの値の要素にごみが入ってしまっています。これを取り除くためにtransform_valuesが有効です。

hash
.group_by{|_, v| v > "09:00" ? "遅刻" : "早着"}
.transform_values{|v| v.map(&:first)}
# =>
# {
#   "遅刻" => ["2018-12-31", "2019-1-2"],
#   "早着" => ["2019-1-1", "2019-1-3", "2019-1-4"]
# }

不均質な主系列と固定された副系列を持つハッシュでは、一貫した写像によって値を書き換えられるような状況は少ないですが、それでも副系列が均質であることがほとんどなので、キーに対する写像の需要はあります。これは均質な主系列を持つハッシュでも当てはまります。例えば、副系列が全て文字列であるold_hashの副系列を全てシンボルにしたいときがあります。古いやり方では、次のようにやるのが1つの方法でした(Ruby on Railsの場合にはHash#symbolize_keysを使うやり方もありますが、純粋なRubyでは使えません。さらに、現在のRuby on RailsのHash#symbolize_keysの実装は以下に述べるHash#transform_keysを使っています)。

hash = {}
old_hash.each {|k, v| hash[k.to_sym] = v}

もう1つのやり方は

hash = old_hash.each_with_object({}){|(k, v), h| h[k.to_sym] = v}

です。これらも、値の写像のときと同様に、エレガントでありません。このようなキーの写像には、Ruby 2.5で導入されたHash#transform_keysが便利です。

hash = old_hash.transform_keys(&:to_sym)

このように、ハッシュの情報からの写像によりハッシュを生成するにはtransform_valuestransform_keysを使うことが出来ますが、これらのメソッドを使ってハッシュ以外の2系列オブジェクトからの写像によりハッシュを生成するには、以下のように、to_hで一旦もとのオブジェクトをハッシュに変換する必要があります。

struct = Struct.new(:foo).new("FOO")
# => #<struct  foo="FOO">
struct
.to_h
.transform_keys(&:to_s)
.transform_values{|v| v * 2}
# => {"foo" => "FOOFOO"}

また、0系列オブジェクトや1系列オブジェクトからの写像ではこのような方法は取れません。そこで、これらのオブジェクトから写像の出来るEnumerable#mapArray#mapを使えますが、そうすると、その返り値である配列に一旦変換してからto_hによりハッシュにする必要があります。

sequence = (0.0..2.0).step(1/3r)
sequence
.map{|t| [t.round(2), Math.exp(t).round(3)]}
.to_h
# => {0.0 => 1.0, 0.33 => 1.396, 0.67 => 1.948, 1.0 => 2.718, 1.33 => 3.794, 1.67 => 5.294, 2.0 => 7.389}

array = %w[a b c]
array
.map{|c| [c.inspect, c.ord]}
.to_h
# => {"\"a\"" => 97, "\"b\"" => 98, "\"c\"" => 99}

これらのやり方では、すぐに捨てるハッシュや配列を途中で生成しているのが無駄です。そこで、Ruby 2.6からEnumerable#to_h, Array#to_h, Struct#to_h, OpenStruct#to_h, ENV.to_h, Hash#to_hがブロックを取って直接にハッシュを生成できるようにしてもらいました。3

sequence.to_h{|t| [t.round(2), Math.exp(t).round(3)]}
# => {0.0 => 1.0, 0.33 => 1.396, 0.67 => 1.948, 1.0 => 2.718, 1.33 => 3.794, 1.67 => 5.294, 2.0 => 7.389}

array.to_h{|c| [c.inspect, c.ord]}
# => {"\"a\"" => 97, "\"b\"" => 98, "\"c\"" => 99}

struct.to_h{|k, v| [k.to_s, v * 2]}
# => {"foo" => "FOOFOO"}

これはハッシュにも恩恵のあることで、キーと値を同時に写像できるようになりました。

old_hash.to_h {|k, v| [k.to_sym, v.some_method]}

それでも、イテレーションごとにキーと値の対を表す配列を作ってすぐに捨てるという無駄をしているので、ハッシュのキーか値の片方だけを写像したいときは、to_hを乱用せずにtransform_keystransform_valuesを使うのが良いです。

個別のオブジェクトからの簡単な対応がない場合

目的のハッシュが、全体としてはもとになる別の1つのオブジェクトの情報に専ら基いていながらも、個別の値には簡単な対応関係がないことがあります。

例えば、文字列:

string = "aaabbabeaaebd"

に含まれるキャラクタの一覧を頻度付きで表すハッシュ:

{"a" => 6, "b" => 4, "e" => 2, "d" => 1}

を生成したいとしましょう。この場合、各キャラクタ(例えば、"a")は、対応するキーの値(頻度、例えば6)に貢献していますが、素直には対応してはいません。

無理に対応させようとすれば、

string.each_char.to_h{|k| [k, string.count(k)]}
# => {"a" => 6, "b" => 4, "e" => 2, "d" => 1}

とすることが出来ますが、これは各イテレーションでkが含まれていたもとの情報であるstringに戻ってアクセスし直すという無駄をし、さらに、頻度の回数だけ(例えば"a"では6回)同じことをやり直し、ハッシュに変化をもたらさないを変更を繰り返すという無駄をしているので、文字列が大きくなると、極端に遅くなります。

あるいは、Ruby 2.6からHash#mergeが任意の数の引数(ハッシュ)を取ることが出来るようになったことを使って、

{}.merge(*string.each_char.map{|k| {k => 1}}){|_, old, new| old + new}
# => {"a" => 6, "b" => 4, "e" => 2, "d" => 1}

とすることが出来ますが、これは途中で一時的な配列やハッシュを生成するので、これも遅いです。

こういうときは、命令型のコードを使いつつも、それをブロック内に閉じ込めるやり方:

string.each_char.inject(Hash.new(0)){|h, k| h[k] += 1; k}
# => {"a" => 6, "b" => 4, "e" => 2, "d" => 1}

もしくは

string.each_char.with_object(Hash.new(0)){|k, h| h[k] += 1}
# => {"a" => 6, "b" => 4, "e" => 2, "d" => 1}

がありますが、この場合はgroup_byが使えます。

string.each_char.group_by(&:itself).transform_values(&:length)
# => {"a" => 6, "b" => 4, "e" => 2, "d" => 1}

この方法は途中で配列を作って捨てるという無駄をしていますが、最後の3つの方法はほぼ同等の速さです。transform_valuesが加わる以前は、この最後の方法に沿った方針で行くのは、コードがやや煩雑になり、魅力がなかったのですが、Ruby 2.6からはこういう方法も実用的に使えるようになりました。

まとめ

ハッシュは配列に似ているものの、情報の系列が1つ多いために、より複雑な生成の仕方があるということを示しました。それにも関わらず、これまで、ハッシュには配列に匹敵するメソッドが十分にありませんでした。近年、その点が改良され続け、先日発表されたRuby 2.6でも新機能が追加され、より自然にコードが書けるようになりました。Rubyがますます面白くなってきました。

最後に

マネーフォワードではエンジニアを募集しています。
ご応募お待ちしています。

【採用サイト】
マネーフォワード採用サイト
Wantedly | マネーフォワード

【マネーフォワードのプロダクト】
自動家計簿・資産管理サービス『マネーフォワード ME』 iPhone,iPad Android

「しら」ずにお金が「たま」る 人生を楽しむ貯金アプリ『しらたま』 iPhone,iPad

おトクが飛び出すクーポンアプリ『tock pop トックポップ』

金融商品の比較・申し込みサイト『Money Forward Mall』

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

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


  1. https://bugs.ruby-lang.org/issues/11818 ↩︎

  2. https://bugs.ruby-lang.org/issues/14559 ↩︎

  3. https://bugs.ruby-lang.org/issues/15143 ↩︎

Pocket