uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Map.withDefault{}は地道に便利

Groovy 1.7.1から、MapにメソッドwithDefault追加されています。

通常、マップにキーが存在していない場合のデフォルト値はJavaの時代からnullと決まってますが、withDefaultでnullではないデフォルト値を設定することができます。

何が嬉しいかというと、たとえば、ワードカウント処理をするとき、今まで

    def map = [:]
   それぞれのワードに対してeach {
        map[it] = map[it]==null ? 1 : map[it] + 1
    }

と書かなければならなかったところを、デフォルト値を0と設定することで

    def map = [:].withDefault{0} 
   それぞれのワードに対してeach {
        map[it]++
    }

とこう書けます。便利。

なお、デフォルト値はクロージャで設定するので、例えばキーに応じて動的にデフォルト値を決めることができます。

withDefaultのデフォルト値設定は、以下のように動作します。

  • withDefault{}で、delegateパターンを用いて、get時にある処理を介在させた新たなマップを作る。
  • 「get時のある処理」とは、「もしキーが存在しなかったらwithDefaultに指定されたクロージャを呼び出し、その返り値をもとのマップの指定したキーの値として突っ込む(そしてその値が返る)」というもの*1。つまり存在しないキーに対するget時に、デフォルト値のput動作を実行します。

結果的に、「メモ化」のようなことをしてくれます。メモ化と言えば、フィボナッチ数の計算の効率化が定番なので、このwithDefaultが提供するメモ化機能を用いてやってみましょう。

fib = [:].withDefault{ k->
   if (k==0) { 0 }
   else if (k==1) { 1 }
   else { fib[k-1]+fib[k-2] }
}

assert fib[16] == 987

(1..20).each {
  println fib[it]
}
// 1,2,2,3,5,8 ...

便利ですね。

*1:クロージャ中で、対象mapに処理対象のキーに対する値を自分で保存しても、上書きされるので注意。