(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

assoc と update どっち使う?

Clojure 1.7 から導入された update 関数。 update-in はあるけど、むしろ今までなんで update がなかったんだろうと思ったりする今日このごろです。

まぁともあれどういうものかというと普通にドキュメント引けば分かりますが、 update-in で指定するキーワードがひとつのときに代わりに使うと良さそうです。

clojure.core/update
([m k f] [m k f x] [m k f x y] [m k f x y z] [m k f x y z & more])
  'Updates' a value in an associative structure, where k is a
  key and f is a function that will take the old value
  and any supplied args and return the new value, and returns a new
  structure.  If the key does not exist, nil is passed as the old value.

で、早速プロジェクトの Clojure のバージョンを 1.7 にあげて、 update 使えるヒャッハー!!してたんですが、 Clojure デキルマンに止められました。
デキルマン「それ assoc で良くないですか?」
ボク「 update の方が意図が伝わりやすいかなーって…」
デキルマン「奇妙に見えるからやめましょう」
ボク「ア、ハイ」
ちなみにどういうコードを書いていたかというとこんな感じ。

(update m :foo (fn [_] x))

まあ確かに奇妙かもしれない。ちなみに定数を返す関数は (constantly x) と書ける。これを assoc で書き直すとこうなる。

(assoc m :foo x)

じゃあ、 assoc と update って何が違うんだろうって思った。ドキュメント引けば答えは書いてある。

clojure.core/assoc
([map key val] [map key val & kvs])
  assoc[iate]. When applied to a map, returns a new map of the
    same (hashed/sorted) type, that contains the mapping of key(s) to
    val(s). When applied to a vector, returns a new vector that
    contains val at index. Note - index must be <= (count vector).

assoc は key に対して新しい val をマッピングしたマップを返すもの。かたや update は既に上で書いた通り、古い値に対して関数を適用し得た新しい値をマッピングしたマップを返すといったもの。

ちょっとだけ具体例を書いてみましょう。

(def x {:foo 10 :bar 20})
;; => #'boot.user/x

(assoc x :foo 30) ;; => {:foo 30, :bar 20}
(update x :foo (constantly 30)) ;; => {:foo 30, :bar 20}

(assoc x :baz 30) ;; => {:foo 10, :bar 20, :baz 30}
(update x :baz (constantly 30)) ;; => {:foo 10, :bar 20, :baz 30}

(defn square [x] (* x x)) ;; => #'boot.user/square
(assoc x :bar (square (:bar x))) ;; => {:foo 10, :bar 400}
(update x :bar square) ;; => {:foo 10, :bar 400}

(assoc x :baz (square (:baz x))) ;; => java.lang.NullPointerException
(update x :baz square) ;; => java.lang.NullPointerException

とまぁこんな感じになります。確かに定数値へと更新する場合は assoc を使ったほうがスマートぽいです。古い値を取得して新しい値に更新するという場合には update の方が便利というわけですね。

元々僕の update 関数に対する理解が悪かったんですが、 update を「 m を f 関数を利用して k にマップされている値を更新する」ではなくて、「 m の k にマップされた値を f 関数で更新する」というふうに理解すれば自ずと使いドコロは見えてくるわけですね。

ちなみに、 update 関数の中身に assoc が使われているので、 assoc で書くのがめんどくさいときのショートハンド(or 構文糖衣)として定義されたと考えるのが普通そうです。

(defn update
  "'Updates' a value in an associative structure, where k is a
  key and f is a function that will take the old value
  and any supplied args and return the new value, and returns a new
  structure.  If the key does not exist, nil is passed as the old value."
  {:added "1.7"
   :static true}
  ([m k f]
   (assoc m k (f (get m k))))
  ([m k f x]
   (assoc m k (f (get m k) x)))
  ([m k f x y]
   (assoc m k (f (get m k) x y)))
  ([m k f x y z]
   (assoc m k (f (get m k) x y z)))
  ([m k f x y z & more]
   (assoc m k (apply f (get m k) x y z more))))