updating nested structure

10 views
Skip to first unread message

Parth Malwankar

unread,
Aug 26, 2008, 11:25:03 AM8/26/08
to Clojure
Hello,

In order to update fields in nested structures/maps easily
I have created a macro 'field-write'.
(defmacro field-write [st k v access-spec]
; st=data, k=key to update, v=val to put, access-spec=access
vector
; ... code listing after interaction

user=> nx
{:a {:b {:c {:content 10}}}}
user=> (field-write nx :content 1000 [:a :b :c])
{:a {:b {:c {:content 1000}}}}
user=> (macroexpand '(field-write nx :content 1000 [:a :b :c]))
(clojure/assoc
nx :a
(clojure/assoc
(clojure/-> nx :a) :b
(clojure/assoc
(clojure/-> nx :a :b) :c
(clojure/assoc
(clojure/-> nx :a :b :c):content 1000))))

[formatting added above for readability]

It seems to be working fine but I thought it may be
good to get inputs from the Clojure experts here to
see if there is a better way to do this in Clojure.

(def nx {:a {:b {:c {:content 10}}}})

(def field-write)
(defmacro field-write [st k v access-spec]
; st=data, k=key to update, v=val to put, access-spec=access
vector
(if (pred/empty? access-spec)
`(assoc ~st ~k ~v)
(field-write st (last access-spec)
`(assoc (-> ~st ~@access-spec) ~k ~v)
(butlast access-spec))))

Thanks.
Parth

Parth Malwankar

unread,
Aug 27, 2008, 4:39:06 AM8/27/08
to Clojure
After some more experimentation I found that the field-write
macro didn't work with access-specs like (vector :a :b :c)
... I should have thought of that before.

So I reimplemented field-read and field-write as functions
which seem to work in all scenarios.

(defn field-read [st as]
"user=> nx
{:a {:b {:c {:data 10}}}}
user=> (field-read nx [:a :b :c])
{:data 10}"
(eval (reduce (comp reverse list) st as)))

(defn field-write [structure kwd value access-spec]
"user=> nx
{:a {:b {:c {:data 10}}}}
user=> (field-write nx :data 20 [:a :b :c])
{:a {:b {:c {:data 20}}}}
user=> (field-write nx :data (+ 2 2 2) (vector :a :b :c))
{:a {:b {:c {:data 6}}}}"
(loop [st structure
k kwd
v value
as access-spec]
(if (pred/empty? as)
(assoc st k v)
(recur st (last as)
(assoc (field-read st as) k v)
(butlast as)))))

I suspect using eval in field-read is a bad idea (preformance?) but I
couldn't come up with a better way to do it. access-spec
needs to be a vector as its something thats returned by
another function in my use case (otherwise I could have
used it with ->).

Can I do something better here? Comments?

Thanks very much.
Parth


> Thanks.
> Parth

Graham Fawcett

unread,
Aug 27, 2008, 9:04:00 AM8/27/08
to clo...@googlegroups.com
On Wed, Aug 27, 2008 at 4:39 AM, Parth Malwankar
<parth.m...@gmail.com> wrote:
> After some more experimentation I found that the field-write
> macro didn't work with access-specs like (vector :a :b :c)
> ... I should have thought of that before.
>
> So I reimplemented field-read and field-write as functions
> which seem to work in all scenarios.
>
> (defn field-read [st as]
> "user=> nx
> {:a {:b {:c {:data 10}}}}
> user=> (field-read nx [:a :b :c])
> {:data 10}"
> (eval (reduce (comp reverse list) st as)))

> I suspect using eval in field-read is a bad idea (preformance?) but I


> couldn't come up with a better way to do it. access-spec
> needs to be a vector as its something thats returned by
> another function in my use case (otherwise I could have
> used it with ->).

No need for eval here:

(defn field-read [st as]
(loop [st st as as]
(if (first as)
(recur (get st (first as))
(rest as))
st)))

Best,
Graham

Chouser

unread,
Aug 27, 2008, 10:33:03 AM8/27/08
to clo...@googlegroups.com
On Tue, Aug 26, 2008 at 11:25 AM, Parth Malwankar
<parth.m...@gmail.com> wrote:
>
> In order to update fields in nested structures/maps easily
> I have created a macro 'field-write'.

It looks like this has come up on IRC a few weeks ago:
http://clojure-log.n01se.net/date/2008-07-15.html#09:22

I had forgotten that when I brought it up again today:
http://clojure-log.n01se.net/date/2008-08-27.html#10:00

It seems to me you're already pretty close to what Rich wants.

--Chouser

Rich Hickey

unread,
Aug 27, 2008, 3:10:28 PM8/27/08
to Clojure
I posted a variant here:

http://paste.lisp.org/display/65964

Rich

Parth Malwankar

unread,
Aug 27, 2008, 10:37:40 PM8/27/08
to Clojure


On Aug 28, 12:10 am, Rich Hickey <richhic...@gmail.com> wrote:

> I posted a variant here:
>
> http://paste.lisp.org/display/65964
>

Rich,

It works very nicely. Thanks.

Just one thought in case the functions args are still being
decided on. Could we consider taking access path as a
vector rather than directly as function args.

Here is the use case I have in mind.

I think most access paths [:a :b :c] would be generated.
Nested structures would be something like

processor -> GPRs (general purpose regs) -> r0 r1 .. rN
-> FPRs (floating point regs) -> f0 f1 .. fN

fridge -> fruits -> apple mango ...
-> veggies -> eggplant ...
-> diary -> milk yoghurt ...

So the developer may set up something like a
(fridge-item-path (get-fruit)) => [:fruits :apple]
(processor-reg-path (get-reg-arg-from-instruction)) => [:gpr-set :r0]

With the current arg handling this is what we would need to do:

user=> (item-path :mango)
[:fruits :mango]

user=> (apply mk-get my-fridge (conj (item-path :mango) :quantity))
30

user=> (apply mk-assoc my-fridge (conj (item-path :mango) :quantity
40))
{:fruits {:mango {:quantity 40, :color :yellow},
:apple {:quantity 20, :color :red}},
:diary-products {:milk {:quantity 1, :color :white,
:type :low-fat},
:yoghurt {:quantity 10, :color :pink,
:type :strawberry}}}

[formatting added above for readability]

In case the access path were vectors the above could become:

(mk-get my-fridge (item-path :mango) :quantity)
(mk-assoc my-fridge (item-path :mango) :quantity new-quantity)

Much less noise.

Its not a big deal as the user would probably be writing a layer
on top of mk-get/mk-assoc e.g. fridge-add-item fridge-check-item
but then with access-paths as vectors it usage will be cleaner.

If you think this is a common enough case it could be considered.

Also, thanks for the [m [k & ks] v] destructuring trick. I didn't know
we could do that. Very neat.

Thanks very much.
Parth

> Rich

Chouser

unread,
Aug 27, 2008, 11:13:49 PM8/27/08
to clo...@googlegroups.com
On Wed, Aug 27, 2008 at 10:37 PM, Parth Malwankar
<parth.m...@gmail.com> wrote:
> In case the access path were vectors the above could become:
>
> (mk-get my-fridge (item-path :mango) :quantity)
> (mk-assoc my-fridge (item-path :mango) :quantity new-quantity)
>
> Much less noise.

apply actually can do the conj'ing for you:

(apply mk-get my-fridge (item-path :mango) :quantity)

--Chouser

Parth Malwankar

unread,
Aug 28, 2008, 2:01:29 AM8/28/08
to Clojure


On Aug 28, 8:13 am, Chouser <chou...@gmail.com> wrote:
> On Wed, Aug 27, 2008 at 10:37 PM, Parth Malwankar
>
> <parth.malwan...@gmail.com> wrote:
> > In case the access path were vectors the above could become:
>
> > (mk-get my-fridge (item-path :mango) :quantity)
> > (mk-assoc my-fridge (item-path :mango) :quantity new-quantity)
>
> > Much less noise.
>
> apply actually can do the conj'ing for you:
>
> (apply mk-get my-fridge (item-path :mango) :quantity)

I get an error with this.

user=> (item-path :mango)
[:fruits :mango]
user=> (apply mk-get my-fridge (item-path :mango) :quantity)
java.lang.IllegalArgumentException: Don't know how to create ISeq
from: Keyword : :quantity
java.lang.IllegalArgumentException: Don't know how to create ISeq
from: Keyword : :quantity
at clojure.lang.RT.seqFrom(RT.java:461)
at clojure.lang.RT.seq(RT.java:444)
at clojure.seq__28.invoke(boot.clj:92)
at clojure.spread__132.invoke(boot.clj:357)
at clojure.spread__132.invoke(boot.clj:358)
at clojure.spread__132.invoke(boot.clj:358)
at clojure.apply__135.doInvoke(boot.clj:364)
at clojure.lang.RestFn.invoke(RestFn.java:460)
at user.eval__2237.invoke(Unknown Source)
at clojure.lang.Compiler.eval(Compiler.java:3847)
at clojure.lang.Repl.main(Repl.java:75)
user=> (apply mk-get my-fridge (conj (item-path :mango) :quantity))
30

Is this supposed to work? If it does it will be very convenient.

Parth

>
> --Chouser

Chouser

unread,
Aug 28, 2008, 10:41:37 AM8/28/08
to clo...@googlegroups.com
On Thu, Aug 28, 2008 at 2:01 AM, Parth Malwankar
<parth.m...@gmail.com> wrote:
>
> On Aug 28, 8:13 am, Chouser <chou...@gmail.com> wrote:
>>
>> (apply mk-get my-fridge (item-path :mango) :quantity)
>
> I get an error with this.
>
> user=> (item-path :mango)
> [:fruits :mango]
> user=> (apply mk-get my-fridge (item-path :mango) :quantity)
> java.lang.IllegalArgumentException: Don't know how to create ISeq
> from: Keyword : :quantity

I'm sorry -- remind me not to post untested code after my bedtime.

You're right, that won't work. Apply does allow you to provide
non-seq arguments followed by the single seq at the end. This is what
I was thinking about:

user=> (apply + 1 2 3 [4 5 6])
21

But the apply expression I wrote above has a seq that you want to
expand early in the list, and non-seq at the end. As you already
discovered, that just throws an exception.

So let me start over -- you want a function that takes a mix of keys
and vectors of keys, right? You could of course wrap the ones Rich
provided for your own use cases. For example if you always have one
vector followed by one plain key, you could do:

user=> (defn mk-get-1 [m v k] (apply mk-get m (concat v [k])))
#'user/mk-get-1
user=> (mk-get-1 my-fridge (item-path :mango) :quantity)
10

Or if you want to allow any kind of mixed vectors, seqs, and plain keys:

user=> (defn mk-get-2 [m & a] (apply mk-get m (mapcat #(if (or
(vector? %) (seq? %)) % [%]) a)))
#'user/mk-get-2
user=> (mk-get-2 my-fridge (item-path :mango) :quantity)
10
user=> (mk-get-2 my-fridge :fruits [:mango :quantity])
10
user=> (mk-get-2 my-fridge :fruits (list :mango :quantity))
10
user=> (mk-get-2 my-fridge :fruits [:mango] :quantity)
10

Neither of these strike me as very general. I don't have my own use
cases (yet) for these functions, so I shouldn't speak too confidently,
but mk-get-1 seems like a pretty specific case (exactly one vector
followed by exactly one plain key). And since vectors are perfectly
valid map keys, it's possible to build nested maps that can't be
accessed using mk-get-2:

user=> (mk-get {[:a :b] :c} [:a :b])
:c
user=> (mk-get-2 {[:a :b] :c} [:a :b])
nil

--Chouser

Rich Hickey

unread,
Aug 28, 2008, 11:08:36 AM8/28/08
to Clojure


On Aug 27, 10:37 pm, Parth Malwankar <parth.malwan...@gmail.com>
wrote:
I don't see this (partial path + separate last key) as a general way
of doing things. The only other way I would consider is the entire
path as a sequence:

(defn mk-get [m ks]
(reduce get m ks))

(defn mk-assoc [m [k & ks] v]
(if ks
(assoc m k (mk-assoc (get m k) ks v))
(assoc m k v)))

;usage
(def nx {:a {:b {:c {:content [1 10] :other 2}}}})

(mk-get nx [:a :b :c :content])
-> [1 10]

(mk-assoc nx [:a :b :c :content 0] 42)
-> {:a {:b {:c {:other 2, :content [42 10]}}}}

(mk-assoc {} [:a :b :x] 42)
-> {:a {:b {:x 42}}}

Rich

Parth Malwankar

unread,
Aug 28, 2008, 11:43:11 AM8/28/08
to Clojure
Thanks Rich.
Sounds like a good idea to me. Works beautifully.

user=> (load-file "fruits.clj")
#'user/my-fridge
user=> (mk-get my-fridge (item-quantity-path :mango))
30
user=> (mk-assoc my-fridge (item-quantity-path :mango) 50)
{:fruits {:mango {:quantity 50, :color :yellow}, :apple {:quantity
20, :color :red}}, :diary-products {:yoghurt {:quantity
10, :color :pink, :type :strawberry}, :milk {:quantity
1, :color :white, :type :low-fat}}}
user=>

Parth

> Rich

Rich Hickey

unread,
Aug 28, 2008, 8:41:17 PM8/28/08
to Clojure


On Aug 28, 11:43 am, Parth Malwankar <parth.malwan...@gmail.com>
I've added get-in, assoc-in, and update-in to SVN rev 1010.

Rich
Reply all
Reply to author
Forward
0 new messages