rubyでpythonのdefaultdictっぽいものを実現する
Pythonのハッシュテーブル(Pythonではdictと言いますね)には、初期値が設定できる亜種としてdefaultdictというものがある。Rubyの場合、Hashクラスには初期値が設定できるが、初期値としては整数か文字列、シンボルぐらいしか使えない。初期値として配列とかハッシュテーブルを使えないのである。
そこで、Pythonのdefaultdictっぽいものを作ってみた。以下にコードを示す。
class DefaultHash < Hash def initialize(init) @init = init end def [](k) v = super k if v v else self[k] = @init.clone end end end a = [1,2,3,4,5,4,3] h = DefaultHash.new(Array.new) a.each{|k| h[k].push 1 } p hash
DefaultHash.newの部分をHash.newに変えると、嬉しい場合があまり思いつかない感じの挙動になっていることがわかると思う。
作ってみたもなにも、[]メソッドを上書きしただけであるし、nilやfalseを値として使った時のことを全く考えていないのだが、とりあえず動くので満足している。
このコードを書いた動機は、以下のようなコードを書きたくない、というものである。
if h.has_key? k h[k].push 1 else h[k] = [1] end
この例ぐらいなら場合分けを書いてもいいんだけど、ちょっと複雑な数え上げがしたくなるとハッシュがネストしたりすることがよくあって、そうなると場合分けの中に場合分けを書くことになって、やりたいことの割にコードが長いというか大仰になってしまってなんだか悲しい、ということがよくあるのだ。
デフォルトのHashでもHash.new(0)みたいに初期値を設定できるのだけど、前述の通り、Array.newみたいなのを引数に渡すとうまくいかないのである。これは同じオブジェクトを使う(ここでの同じ、という言葉はメモリアドレスが同じという意味だと考えてもらいたい。値が同じ、とかではなく、同じ物を指しているのである)のが原因である。
なぜデフォルトの挙動では初期値として同じオブジェクトを使っているのかというと、が呼ばれる度に新しいオブジェクトを作っていたのでは、存在しないキーに対してを呼ぶ度に新しいオブジェクトを作ることになるからだ。これでは、場合によってはリークに近い挙動を示すことになってしまう。という訳で、Hashクラスでは初期値(デフォルトオブジェクトと言い換えたほうがわかりやすいかな)は共有されるのである。
valueが初期値であるようなkeyはGCみたいな感じで定期的にdeleteすればいいのかなーと思うが、面倒なのでそこまでは実装してない。10000回ぐらい[]が呼ばれたらスイープする、みたいな実装だったらパフォーマンス的にも我慢できるぐらいになるんではなかろうかと思うんだけど。