エンジニアの澤田です。
この連載では、社内の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系列オブジェクトには、ハッシュインスタンスや、ENV
、OpenStruct
のインスタンス、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
とすれば、三項演算子を使わずに済み、また他のパラメーター値を与えるコードの部分との違いを縮小できます。... || nil
はfalse
をnil
に変換する働きをします。!params[:param_k].nil? || nil
で、params[:param_k]
を「nil
以外対nil
」に場合分けし、「true
対nil
」に変換しています。でもこれはあまりエレガントではありませんね。それは次の節で解決します。
制御構造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のメソッド評価の仕方が関係しています。
定式化してみましょう。ローカル変数p
がP
というクラスのインスタンスかもしくはnil
を表す可能性があり、q
がP
のインスタンスメソッドではあるが、nil
に対しては定義されていないとします。このとき、次のように、p.q
をレシーバー、p
を引数として取り、p
がnil
である場合にp.q
の評価を回避するメソッドif
:
p.q.if(p)
も、次のようにp
をレシーバー、p.q
を引数として取り、p
がnil
である場合にp.q
の評価を回避するメソッドand
:
p.and(p.q)
も存在しません。
背理法で示します。命題に反して、上のようなif
, and
メソッドがあるとします。Rubyがメソッド評価をするとき、まずレシーバーを評価し、次に引数を評価し、その後にそれらをメソッドに引き渡して、メソッドの評価をします。if
メソッドの場合もand
メソッドの場合も、メソッドが評価され始めたときは、p
及びp.q
が評価された後です。従って、if
やand
がその内部でいかなることを行おうとも、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)
はfoo
がnil
の場合には 、引数baz
やメソッドbar
の評価を回避し、nil
を返します。それ以外の(foo
がfalse
などの)場合には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#compact
やHash#compact!
は、古くからあるArray#compact
やArray#compact!
から類推したもので、Array#compact
などがnil
である要素を除くのと同様に、Hash#compact
などは、 nil
な値と対応するキーを除きます。Ruby on Railsでも同じ機能が実装されてきましたが、Ruby内部で実装され、フレームワークに関わりなく使えるようになったことに意義があります。
主系列が均質な場合で気になった次のような例があります。
[見かけた書き方]
hash = {} old_hash.each {|k, v| hash[k] = v if v}
ここでold_hash
はnil
な値を含んだハッシュです。この場合、
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#reject
やENV.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_values
やtransform_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#map
やArray#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_keys
やtransform_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』
■ビジネス向けクラウドサービス『マネーフォワードクラウドシリーズ』 ・バックオフィス業務を効率化『マネーフォワードクラウド』 ・会計ソフト『マネーフォワードクラウド会計』 ・確定申告ソフト『マネーフォワードクラウド確定申告』 ・請求書管理ソフト『マネーフォワードクラウド請求書』 ・給与計算ソフト『マネーフォワードクラウド給与』 ・経費精算ソフト『マネーフォワードクラウド経費』 ・マイナンバー管理ソフト『マネーフォワードクラウドマイナンバー』 ・資金調達サービス『マネーフォワードクラウド資金調達』