多段階の Hash が欲しい

多段階の Hash を 1 回の処理で欲しかったが、やり方がピンと来ない。
調べてみてそのまま使えば、動きはしたものの「コピペ」には責任が伴うものであり、その内容を詳しく見てみることにした。

参考

欲しかったもの

段階を踏まずに 1 回で深いところの hash まで設定できるようにしたい。

1
2
3
4
5
6
7
# このコードは動かない
h = {}

h["a"]["a"] = 1
h["a"]["b"] = 1

p h

欲しかったもの - その答え

多段のハッシュを 1 回で得るには、次の記述でよい。

1
2
3
4
5
6
7
h = Hash.new { |hash,key| hash[key] = Hash.new(&hash.default_proc) }

h["a"]["a"] = 1
h["a"]["b"] = 1

p h
# => {"a"=>{"a"=>1, "b"=>1}}

何をしているのか、釈然としないので、いろいろ調べる

Ruby の Hash クラスには、ブロックを渡すことができる。
ブロックを渡すと、対応するキーがなかったときに、呼び出しされる処理を定義できる。
このブロック形式のデフォルト値のことをデフォルトブロックと記述されていることがある。
設定されていないときは、nil が返る。

1
2
3
h = Hash.new
p h.default_proc
# => nil

試しに、設定してみる。
設定すると、.default_proc が Proc を返す。

1
2
3
4
5
6
7
8
h = Hash.new {|hash,key| p key}
p h.default_proc
# => #<Proc:0x00005568efffaa98 app.rb:16>

# 無いものを指定したらkeyを表示してくれるのを想定したが、エラーになる
h["a"]["b"] = 1
# => エラー

読んでいくと、どうやらブロックは、Hash を返す必要がありそう。
確かに、ドキュメントには、new {|hash, key| ... } -> Hash の記載がある。

1
2
3
4
5
h = Hash.new {|hash,key| Hash.new}
h["a"]["b"] = 1

p h
# => {}

Hash.new を返してしまったので、ずっと空になってしまう。
若干予想と異なった動きをしたのはこちら。

1
2
3
4
5
6
7
8
9
10
11
h = Hash.new {|hash,key| {a: "V"}}
p h
# => {}

p h[:a]
# => {:a=>"V"}

h[:b] = 1

p h = 1
# => {:b=>1}

この動きは、h[:a] のように値が無い場合、{a: "V"}を返すという形になっているだけだった。

次の 場合、h[:a] を呼び出したとき、その値が、"Value" だと定義しています。
設定をすると値が入ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
h = Hash.new {|hash,key| hash[key] = "Value" }
p h
# => {}

h[:a]

p h
# => {:a=>"Value"}

h = Hash.new {|hash,key| hash[key] = "Value" }
p h
# => {}

h[:a] = 1

p h
# => {:a=>1}

次のようにすると 2 段目の呼び出しまで対応します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

h = Hash.new {|hash,key| hash[key] = {} }
p h
# => {}

h[:a]

p h
# => {:a=>{}}

h = Hash.new {|hash,key| hash[key] = {} }
p h

h[:a][:b]

p h
# => {:a=>{}}

h = Hash.new {|hash,key| hash[key] = {} }
p h

h[:a][:b][:c] = 1
# => `<main>': undefined method `[]=' for nil:NilClass (NoMethodError) 3団目は無理
p h

次の作り方で、3 段目まで対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
h = Hash.new {|hash,key| hash[key] = Hash.new {|hash,key| hash[key] = Hash.new {} } }
p h
# => {}
h[:a]

p h
# => {:a=>{}}

h = Hash.new {|hash,key| hash[key] = Hash.new {|hash,key| hash[key] = Hash.new {} } }
p h
# => {}

h[:a][:b]

p h
# => {:a=>{:b=>{}}}

h = Hash.new {|hash,key| hash[key] = Hash.new {|hash,key| hash[key] = Hash.new {} } }
p h
# => {}

h[:a][:b][:c]

p h
# => {:a=>{:b=>{}}}

h = Hash.new {|hash,key| hash[key] = Hash.new {|hash,key| hash[key] = Hash.new {} } }
p h
# => {}

h[:a][:b][:c][:d]
# => ここでエラー
p h

と、これ以上深くするなら、何度も多重に書くこととなります。

ここで問題のコードを再度見る。

1
h = Hash.new { |hash,key| hash[key] = Hash.new(&hash.default_proc) }

ここまでの確認をしたことで、理解が及ぶようになった。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
h = Hash.new { |hash,key| hash[key] = Hash.new(&hash.default_proc) }

# 解釈
# 1. ここでの h = には ブロックを設定した hash オブジェクトが渡っている
# 2. ブロック { |hash,key| hash[key] = Hash.new(&hash.default_proc) } は、
# Hash.new で作成した Hash オブジェクトをhash[key]に設定
# Hash.new に設定されている &hash.default_proc は、
# { |hash,key| hash[key] = Hash.new(&hash.default_proc) } ブロック自体である。
# &hoge は、ブロック引数
#
#
# 3. 以上により何段階でも深いHashオブジェクトが作れる
#
#

ということは、こういう書き換えもできる。

1
2
3
4
5
6
7
8
9
10
11
12
13
p = Proc.new {|hash,key| p "AAA"; hash[key] = Hash.new &hash.default_proc  }

h = Hash.new &p


h["a"]["a"] = 1
# =>"AAA" <=このタイミングで h[a] = Hash.new &hash.default_proc が起きている
h["a"]["b"] = 1
h["a"]["c"]["d"] = 1
# =>"AAA" <=このタイミングで h[a][c] = Hash.new &hash.default_proc が起きている

p h
# => {"a"=>{"a"=>1, "b"=>1, "c"=>{"d"=>1}}}

とここまで来て完全に納得。
日常的に、毎回 Proc オブジェクトを分離する理由も無いので普段書くならこちらで良さそう。

1
h = Hash.new { |hash, key| hash[key] = Hash.new(&hash.default_proc) }

で、それを使って何をしたかったのか?

こんなテーブルが有ったとき。

ユーザー ID 学年 クラス 教科 点数
1000001 1 A Math 20
1000002 1 A Math 40
1000001 1 A Science 20
1000002 1 A Science 50
1000003 1 B Science 20
1000004 2 A Math 40

学年、クラス、教科で group 化するとこんなデータができる。

1
[[[1,A,Math],30], [[1,A,Science],35], [[1,B,Science],20], [[2,A,Math],40]]

これをハッシュで表現したいので、多段ハッシュを作れるようにしておくと次の方法で処理できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
src = [[[1,"A","Math"],30], [[1,"A","Science"],35], [[1,"B","Science"],20], [[2,"A","Math"],40]]

result = Hash.new { |hash, key| hash[key] = Hash.new(&hash.default_proc) }

src.each do | data |
result[data[0][0]][data[0][1]][data[0][2]] = data[1]
end

p result
# => {1=>{"A"=>{"Math"=>30, "Science"=>35}, "B"=>{"Science"=>20}}, 2=>{"A"=>{"Math"=>40}}}

p result[1]["A"]["Math"]
# => 30

多段のハッシュをまとめて作れないと、各階層のキーが存在するか全て確認することになってしまう。

さらに、active_support を導入しておくと、文字列とシンボルどちらでも対応できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require 'active_support'
require 'active_support/core_ext'

src = [[[1,"A","Math"],30], [[1,"A","Science"],35], [[1,"B","Science"],20], [[2,"A","Math"],40]]

result = Hash.new { |hash, key| hash[key] = Hash.new(&hash.default_proc).with_indifferent_access }

src.each do | data |
result[data[0][0]][data[0][1]][data[0][2]] = data[1]
end

p result
# => {1=>{"A"=>{"Math"=>30, "Science"=>35}, "B"=>{"Science"=>20}}, 2=>{"A"=>{"Math"=>40}}}

p result[1]["A"]["Math"]
# => 30

p result[1][:A][:Math]
# => 30 <= シンボルでもOK

というわけで、多段階のハッシュを1回で作成をする方法を書いた記事を見つけたが、コピペはいやなのでいろいろ調べてみた。
理解が追いついたので、ぜひ使っていきたい。

ではでは。