Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

【Ruby入門】Array#assocとArray#rassocで配列をハッシュのように扱う方法

配列に大量のデータを格納しているけれど、ハッシュのようにキーと値のペアで検索したい——そんな経験はありませんか? 実はRubyには、配列をキー/バリュー構造のように扱える便利な仕組みが用意されています。この記事では、その使い方をわかりやすく解説します。

Array#assoc と Array#rassoc とは

ここでは、数分ごとに「買え」「売れ」という銘柄推奨を出してくれる、架空の株予想マシンを例に考えてみましょう。このマシンをコンピュータに接続すると、次のようなデータのストリームを受け取れるとします。

picks = [
  ["AAPL", "buy"],
  ["GOOG", "sell"],
  ["MSFT", "sell"]
]

Google(GOOG)に関する最新の推奨を取得したい場合は、Array#assoc メソッドを使います。各行の先頭要素(row[0])が指定したキーと一致する最初の行を返してくれます。

# row[0] == "GOOG" となる最初の行を返す
picks.assoc("GOOG") # => ["GOOG", "sell"]

逆に、「sell」という推奨が含まれる行を探したい場合は Array#rassoc メソッドを使います。こちらは各行の2番目の要素(row[1])と照合します。

# row[1] == "sell" となる最初の行を返す
picks.rassoc("sell") # => ["GOOG", "sell"]

該当する行が見つからなかった場合、どちらのメソッドも nil を返します。

picks.assoc("CSCO") # => nil
picks.rassoc("hold") # => nil

履歴データを残しながら最新の値を取得する

ハッシュには「同じキーに対して1つの値しか持てない」という制約があります。一方、配列なら重複したキーをいくらでも持てます。assoc / rassoc は賢いことに、複数マッチした場合でも最初に見つかった行だけを返します。これを利用すると、かなり面白いことができます。

例の株予想マシンは、時間が経つと判断を覆すことがあります。以前「売れ」と言っていた銘柄を、後になって「買え」と推奨し直すかもしれません。その場合、データは次のようになります。

picks = [
  ["GOOG", "buy"],
  ["AAPL", "sell"],
  ["AAPL", "buy"],
  ["GOOG", "sell"],
  ["MSFT", "sell"]
]

もしハッシュにこのデータを放り込むと、特定の銘柄の推奨を更新した時点で、それまでの履歴は失われてしまいます。しかし配列なら話は別です。新しい推奨を配列の先頭に追加していけば、Array#assoc は常に最新の推奨を返してくれるからです。

# row[0] == "GOOG" となる最初の行=最新の推奨を返す
picks.assoc("GOOG") # => ["GOOG", "buy"]

つまり、ハッシュのようなキー/バリュー検索の手軽さに加えて、監査ログ(変更履歴)まで無料で手に入るわけです。

3列以上のデータにも対応

assoc のもう一つの魅力は、各行のカラム数が2つに限定されない点です。何列でも自由に持たせられます。たとえば、売買の推奨にタイムスタンプを付けてみましょう。

picks = [
  ["AAPL", "buy", "2015-08-17 12:11:55 -0700"],
  ["GOOG", "sell", "2015-08-17 12:10:00 -0700"],
  ["MSFT", "sell", "2015-08-17 12:09:00 -0700"]
]

この状態で assocrassoc を使うと、タイムスタンプも含めた行全体が返ってきます。

# 行全体が返される
picks.assoc("GOOG") # => ["GOOG", "sell", "2015-08-17 12:10:00 -0700"]

CSVなど、多数のカラムを持つファイル形式のデータを扱う際に、この挙動がどれほど役立つか想像できるでしょう。

パフォーマンス:ハッシュとの速度比較

ほとんどのベンチマークにおいて、Rubyのハッシュは Array#assoc より高速です。データセットが大きくなるほど差は顕著になります。ハッシュテーブルの検索計算量は O(1) であるのに対し、配列の線形検索は O(n) だからです。ただし、ケースによってはその差が実用上問題にならないことも多く、実際の判断はデータの規模やアクセスパターン次第です。

参考までに、10行の小さなデータセットと10万行の大きなデータセットで、ハッシュ検索と assoc の速度を比較する簡単なベンチマークを実行してみました。結果は予想通り、小さいデータセットでは両者とも良好な性能を示しましたが、大きいデータセットではハッシュが圧勝しました。

…とはいえ公平を期すために言うと、このベンチマークでは配列の最後尾の要素を検索しています。これは配列検索にとって最悪のケースであり、実測値は悲観的に見積もられています。

require 'benchmark/ips'
require 'securerandom'

Benchmark.ips do |x|
  x.time = 5
  x.warmup = 2

  short_array = (0..10).map { |i| [SecureRandom.hex(), i] }
  short_hash = Hash[short_array]
  short_key = short_array.last.first

  long_array = (0..100_000).map { |i| [SecureRandom.hex(), i] }
  long_hash = Hash[long_array]
  long_key = short_array.last.first

  x.report("short_array") { short_array.assoc(short_key) }
  x.report("short_hash") { short_hash[short_key] }
  x.report("long_array") { long_array.assoc(long_key) }
  x.report("long_hash") { long_hash[long_key] }

  x.compare!
end


# Calculating -------------------------------------
#          short_array    91.882k i/100ms
#           short_hash   149.430k i/100ms
#           long_array    19.000  i/100ms
#            long_hash   152.086k i/100ms
# -------------------------------------------------
#          short_array      1.828M (± 3.4%) i/s -      9.188M
#           short_hash      6.500M (± 4.8%) i/s -     32.426M
#           long_array    205.416  (± 3.9%) i/s -      1.026k
#            long_hash      6.974M (± 4.2%) i/s -     34.828M

# Comparison:
#            long_hash:  6974073.6 i/s
#           short_hash:  6500207.2 i/s - 1.07x slower
#          short_array:  1827628.6 i/s - 3.82x slower
#           long_array:      205.4 i/s - 33950.98x slower

まとめ

  • Array#assoc:各行の先頭要素をキーとして検索し、一致した最初の行を返す
  • Array#rassoc:各行の2番目の要素をキーとして検索し、一致した最初の行を返す
  • 見つからない場合は nil を返す
  • 重複キーを許容するため、履歴を保持しながら最新値を取得できる
  • カラム数の制限がないため、CSVなどの多列データとの相性が良い
  • 大規模データではハッシュの方が高速なので、用途に応じて使い分けるのが賢明

小〜中規模のデータや履歴管理が必要な場面では、assoc / rassoc はハッシュでは実現できない柔軟性を提供してくれます。ぜひ活用してみてください。

  1. Ruby 2.6のMJITとは?仕組みとパフォーマンスを徹底解説

    Rubyのパフォーマンスは、バージョンが上がるごとに大きく向上してきました。そしてRuby開発チームは、さらに速いRubyを実現するために全力を尽くしています。 その取り組みのひとつが「3×3プロジェクト」です。 その目標とは? Ruby 3.0を、Ruby 2.0の3倍速くすることです。 このプロジェクトの一環として誕生したのが、新しいMJITコンパイラです。これが本記事のテーマとなります。 MJITとは何か MJITは「Method Based Just-in-Time Compiler(メソッドベースのジャストインタイムコンパイラ)」の略称です。 これは具体的にどういう意味でしょうか?

  2. Ruby on Railsとは?初心者にもわかる仕組み・魅力・学び方を徹底解説

    Ruby on Railsとは? Ruby on Rails(略称:RoR)は、世界で最も人気のあるオープンソースのWebアプリケーションフレームワークです。プログラミング言語「Ruby」をベースに構築されており、シンプルなサイトから大規模で複雑なサービスまで、幅広いWebアプリケーションの開発を支援します。 そもそもフレームワークとは? フレームワークとは、ソフトウェア開発の際に土台となる構造を提供してくれるコードやツール、ユーティリティの集合体です。あらかじめ用意された構造に沿ってコードを書くことで、プログラムが整理され、保守性も高まります。正しく使いこなせるようになれば、開発作業は格段に