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

Ruby Hashの高度なテクニック徹底解説 ―― デフォルト値の制御から遅延キャッシュまで

Ruby開発者ほどHash(ハッシュ)を頻繁に使うと、「もう全部知っている」と思い込みがちです。

しかし、一見地味なRubyのHashには、意外なほど多くの隠れた技が詰まっています。単なるキー・バリューの仕組みにとどまらず、Hashオブジェクトを使えば、非常に興味深く洗練された処理を実現できるのです。

あらゆるオブジェクトがハッシュのキーになる

本題に入る前に、見落とされがちなポイントを1つ指摘しておきましょう。ハッシュのキーには文字列やシンボルを使うことが多いものの、それは他の種類のオブジェクトが使えないという意味ではありません。実際、ほとんど何でもキーにできるのです。

# 数値もハッシュのキーになる
{1 => "one"}[1] # "one"

# Rubyカーネル(Kernelモジュール)もOK
{Kernel => 1}[Kernel] # 1

# 特定のクラスごとに値を保存できる
{Kernel => 1, String => 2}["hello world".class] # 2

# 真偽値もキーにできる
{true => "verdad"}[1==1] # "verdad"

# 複雑な配列や、他のハッシュすらキーとして使える
{[[1,0],[0,1]] => "identity matrix"}[[[1,0], [0,1]]] # "identity matrix"

実用性には差がありますが、いずれの方法も自由に使えます。

デフォルト値を自由にコントロールする

h = { a: 1 }というハッシュがあり、存在しない値にアクセスした場合――たとえばh[:x]――戻り値はnilになります。これは、特に指定しない限り、すべてのハッシュのデフォルト値がnilだからです。

新しいハッシュを作るときにコンストラクタへ引数を渡せば、デフォルト値を設定できます。

h = Hash.new("この属性は意図的に空欄にしています")
h[:a] = 1
h[:a] # 1
h[:x] # "この属性は意図的に空欄にしています"

動的なデフォルト値

ここが肝心です。このテクニックは、これから紹介するすべての応用の土台になります。

コンストラクタにブロックを渡すと、デフォルト値をプログラム的に生成できます。次の例ではデフォルト値にタイムスタンプを付けているので、アクセスのたびに値が動的に生成されているのが分かります。

h = Hash.new { |hash, key| "#{key}: #{ Time.now.to_i }" }
h[:a] # "a: 1435682937"
h[:a] # "a: 1435682941"
h[:b] # "b: 1435682943"

重要なのは、「デフォルト値」用のブロックは、単に値を返す以外のこともできるという点です。

キーが存在しない場合に例外を発生させる

ハッシュの大きな問題のひとつは、エラーが静かに握りつぶされてしまうことです。user[:phone]と打つべきところをうっかりuser[:phnoe]とタイプミスしても、例外は発生せず、ハッシュはnilを返してしまいます。しかし、この挙動は変更できます。

h = Hash.new { |hash, key| raise ArgumentError.new("No hash key: #{ key }") }
h[:a]=1
h[:a] # 1
h[:x] # ArgumentError: No hash key: x が発生

この手法はデバッグやリファクタリングで役立ちます。特定のハッシュだけに適用できるため、Hashクラスをモンキーパッチするようなやり方に比べて、はるかに影響範囲の小さい方法だからです。

補足:新しいコードでは、この方法の代わりにHash#fetchを使うことをおすすめします。あくまでデバッグやリファクタリング時に使える「とっておきの小技」と考えてください。

遅延生成されるルックアップテーブル

このテクニックは、計算結果のキャッシュに便利です。大量の平方根を計算する必要があるとしましょう。次のような、必要になった時点で初めて値を埋めていく遅延型のルックアップテーブルを作れます。

sqrt_lookup = Hash.new { |hash, key| hash[key] = Math.sqrt(key) }
sqrt_lookup[9] # 3.0
sqrt_lookup[7] # 2.6457513110645907
sqrt_lookup    # {9=>3.0, 7=>2.6457513110645907}

再帰的な遅延ルックアップテーブル

再帰関数があり、各再帰の結果をキャッシュしたい場面を考えてみましょう。例として階乗の計算を挙げます。「4の階乗」、いわゆる「4!」とは「4×3×2×1」のことです。ハッシュを使えば、これを再帰的に実装できます。以下の例はあるブログ記事から引用したもので、この考え方をうまく示しています。

factorial = Hash.new do |h,k| 
  if k > 1
    h[k] = h[k-1] * k
  else
    h[k] = 1
  end
end

factorial[4] # 24
factorial    # {1=>1, 2=>2, 3=>6, 4=>24}

初期化後にもデフォルト値を変更できる

ハッシュを生成した後でも、デフォルト値を操作できます。その場合はdefaultおよびdefault_procのセッターを使います。

h={}
h[:a] # nil
h.default = "新しいデフォルト値"
h[:a] # "新しいデフォルト値"

h.default_proc = Proc.new { Time.now.to_i }
h[:a] # 1435684014

ルビーを探せ:遅延的に無限にネストするハッシュで作るゲーム

最後に、お遊びとして、ここまで紹介した便利なテクニックを全部盛り込んだ、極めて無用なサンプルを作ってみましょう。懐かしのテキストアドベンチャーゲーム「Adventure」を覚えていますか? その「世界一バカバカしい版」を作ってみます。

あなたは洞窟の中にいます。北・南・東・西のどちらかに進めます。3つの選択肢は新たな「部屋」へと続き、探索は続いていきます。しかし、残りの1つの選択肢だけは「ルビー」へとたどり着きます。だからこそ、このゲームの名前は「ルビーを探せ(Find the Ruby)」なのです。

洞窟の各部屋はそれぞれ1つのハッシュに対応しています。ハッシュにはエントリが1つだけあり、"n"、"s"、"e"、"w"の中からランダムに選ばれた1つのキーの値が「ルビーを見つけました!」になっています。間違った方向を選ぶと、新しいハッシュが生成されてツリーに追加されていきます。

generator = Proc.new do |hash, key| 
  hash[key] = Hash.new(&generator).merge(["n", "s", "e", "w"][rand(4)] => "You found the ruby!")
end
dungeon = Hash.new(&generator)
dungeon["n"] # <Hash ...
dungeon["n"]["s"] # <Hash ...
dungeon["n"]["s"]["w"] # "You found the ruby!"
  1. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端

  2. ハッシュテーブルの仕組みを徹底解説!ハッシュ関数・バケット・衝突処理の基礎

    私が特に好きなデータ構造のひとつがハッシュテーブルです。シンプルでありながら非常に強力だからです。 キーと値のペアを効率的に保存できる手段として、あなたもすでに使ったことがあるかもしれません。 実は、ハッシュテーブルの実装には学ぶ価値のある興味深いコンピュータサイエンスの概念がたくさん詰まっています。この記事では、その仕組みを一緒に掘り下げていきましょう! バケットとハッシュ関数 ハッシュテーブルの基本的な考え方は、キーでインデックス付けされたデータに対して、O(1) の計算量で効率的にアクセスできるようにすることです。 おさらいとして、Ruby でハッシュテーブルを使うと次のような見た目にな