map literals, computed keys and side-effects

34 views
Skip to first unread message

Christophe Grand

unread,
Feb 17, 2009, 2:03:44 PM2/17/09
to clo...@googlegroups.com
While kicking the tires, I encountered this "behavior":

user=> (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 3 3 4 4 5 5 6
6 7 7 8 8 9 9})
{3 3, 4 4, 5 5, 6 6, 7 7, 8 8, 9 9, 1 2}
user=> (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 3 3 4 4 5 5 6
6 7 7 8 8})
{1 1, 2 2, 3 3, 4 4, 5 5, 6 6, 7 7, 8 8}

Granted, it's not casual Clojure code but it's surprising.

Christophe

--
Professional: http://cgrand.net/ (fr)
On Clojure: http://clj-me.blogspot.com/ (en)


Christian Vest Hansen

unread,
Feb 17, 2009, 2:28:22 PM2/17/09
to clo...@googlegroups.com
That's odd.

Might you have uncovered a bug regarding:

user=> (class {1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9})
clojure.lang.PersistentHashMap
user=> (class {1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8})
clojure.lang.PersistentArrayMap

While with the atom code we have:

user=> (class (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 3 3 4
4 5 5 6 6 7 7 8 8 9 9}))
clojure.lang.PersistentArrayMap
user=> (class (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 3 3 4
4 5 5 6 6 7 7 8 8}))
clojure.lang.PersistentArrayMap
--
Venlig hilsen / Kind regards,
Christian Vest Hansen.

Christian Vest Hansen

unread,
Feb 17, 2009, 2:50:52 PM2/17/09
to clo...@googlegroups.com
I think I got it :)

The two (swap! a inc) forms are added to the map at read-time - which
is before they are evaluated. However, since we are associating the
(swap! a inc) key with a value twice, only the last one counts. So the
atom is inc'ed to 1 once (because keys can only be in the map once),
and assoc'ed with the value 2 (because that's the second assoc we're
doing to the (swap! a inc) form).

The PersistantArrayMap, on the other hand, is more gullible and
trusting towards the parsed array of forms that the reader presents
it, and therefor accepts the (swap! a inc) key twice, which in turn
causes it to evaluate twice; into two different keys with two
different associations.

Christian Vest Hansen

unread,
Feb 17, 2009, 2:59:57 PM2/17/09
to clo...@googlegroups.com
Just to clarify; I think PersistentArrayMap is too naïve:

user=> {1 1 1 1 1 1 2 2}
{1 1, 1 1, 1 1, 2 2}

Also, this is rev 1286 (just prior to lazy-branch merge thingy).

On Tue, Feb 17, 2009 at 8:50 PM, Christian Vest Hansen

Christophe Grand

unread,
Feb 17, 2009, 3:08:53 PM2/17/09
to clo...@googlegroups.com
Christian Vest Hansen a écrit :

> Just to clarify; I think PersistentArrayMap is too naïve:
>
> user=> {1 1 1 1 1 1 2 2}
> {1 1, 1 1, 1 1, 2 2}
>
> Also, this is rev 1286 (just prior to lazy-branch merge thingy).
>

Fixing array-map would make the two tests consistent but I'm not sure
that (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 }) should
evaluate to {1 2}.

Christian Vest Hansen

unread,
Feb 17, 2009, 3:30:57 PM2/17/09
to clo...@googlegroups.com
On Tue, Feb 17, 2009 at 9:08 PM, Christophe Grand <chris...@cgrand.net> wrote:
>
> Fixing array-map would make the two tests consistent but I'm not sure
> that (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 }) should
> evaluate to {1 2}.

That would make it two distinct issues, as I see it. One for
array-map, and one for whether association happens before or after the
keys have been evaluated.

It would seem really strange to me if the following behavior is correct:

user=> (def m (array-map 1 1 1 2 1 3))
#'user/m
user=> (m 1)
1
user=> (m 2)
nil
user=> m
{1 1, 1 2, 1 3}

So I went ahead and opened an issue for that:
http://code.google.com/p/clojure/issues/detail?id=83

I agree it would be nice if the keys were evaluated before the map was
created - python certainly does it that way, though it isn't a lisp.
Clojure is the only lisp I'm familiar with, so I can't say what's
normal here.

>
> Christophe
>
> --
> Professional: http://cgrand.net/ (fr)
> On Clojure: http://clj-me.blogspot.com/ (en)
>
>
>
> >
>



Rich Hickey

unread,
Feb 17, 2009, 4:07:07 PM2/17/09
to Clojure


On Feb 17, 3:30 pm, Christian Vest Hansen <karmazi...@gmail.com>
wrote:
> On Tue, Feb 17, 2009 at 9:08 PM, Christophe Grand <christo...@cgrand.net> wrote:
>
> > Fixing array-map would make the two tests consistent but I'm not sure
> > that (let [a (atom 0)] {(swap! a inc) 1 (swap! a inc) 2 }) should
> > evaluate to {1 2}.
>
> That would make it two distinct issues, as I see it. One for
> array-map, and one for whether association happens before or after the
> keys have been evaluated.
>
> It would seem really strange to me if the following behavior is correct:
>
> user=> (def m (array-map 1 1 1 2 1 3))
> #'user/m
> user=> (m 1)
> 1
> user=> (m 2)
> nil
> user=> m
> {1 1, 1 2, 1 3}
>
> So I went ahead and opened an issue for that:http://code.google.com/p/clojure/issues/detail?id=83
>

Please don't create issues without getting a nod from me here first.

These are bugs in user code. Map literals are in fact read as maps, so
a literal map with duplicate keys isn't going to produce an evaluated
map with distinct keys. If you create an array map with duplicate
keys, bad things will happen.

I'm not sure I want to slow down array-map in order to add the check.

Rich

Christian Vest Hansen

unread,
Feb 17, 2009, 5:03:00 PM2/17/09
to clo...@googlegroups.com
On Tue, Feb 17, 2009 at 10:07 PM, Rich Hickey <richh...@gmail.com> wrote:
>
>
>
> On Feb 17, 3:30 pm, Christian Vest Hansen <karmazi...@gmail.com>
> Please don't create issues without getting a nod from me here first.

Ok. I won't. I must have overlooked the "Similarly, please confirm a
bug before making an entry." part.

>
> These are bugs in user code. Map literals are in fact read as maps, so
> a literal map with duplicate keys isn't going to produce an evaluated
> map with distinct keys. If you create an array map with duplicate
> keys, bad things will happen.
>
> I'm not sure I want to slow down array-map in order to add the check.

My opinion is unchanged, even though I always end up being in the
wrong when we argue.

>
> Rich

Timothy Pratley

unread,
Feb 17, 2009, 6:24:59 PM2/17/09
to Clojure
I don't understand your example:
> user=> (class {1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9})
> clojure.lang.PersistentHashMap
> user=> (class {1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8})
> clojure.lang.PersistentArrayMap

What governs which class {} will return? The number of arguments?

Stephen C. Gilardi

unread,
Feb 17, 2009, 6:39:06 PM2/17/09
to clo...@googlegroups.com

On Feb 17, 2009, at 6:24 PM, Timothy Pratley wrote:

> What governs which class {} will return? The number of arguments?

Yes. The current implementation of Clojure's literal map reader
returns a PersistentArrayMap if there are 8 entries or fewer and a
PersistentHashMap otherwise.

--Steve

Timothy Pratley

unread,
Feb 17, 2009, 8:01:19 PM2/17/09
to Clojure
Thanks Steve,

> returns a PersistentArrayMap if there are 8 entries or fewer and a  
> PersistentHashMap otherwise.

Ah that makes sense. Thanks for the explination.

"The two (swap! a inc) forms are added to the map at read-time -
which
is before they are evaluated."

What's the reason for this? Is it because {} is a special form and so
behaves similar to a macro instead of a function in terms of argument
evaluation?
user=> (let [a (atom 0)] (hash-map (swap! a inc) 1 (swap! a inc) 2 3 3
4 4 5 5 6 6 7 7 8 8 9 9))
{1 1, 2 2, 3 3, 4 4, 5 5, 6 6, 7 7, 8 8, 9 9}
Is the reason for the different evaluation order performance related
for maps?
user=> (time (dotimes [i 1000000] {i :fun}))
"Elapsed time: 123.648676 msecs"
user=> (time (dotimes [i 1000000] (hash-map i :fun)))
"Elapsed time: 517.765247 msecs"
user=> (time (dotimes [i 1000000] (array-map i :fun)))
"Elapsed time: 414.936815 msecs"
Certainly seems to be faster to use {} than hash-map so I'm thinking
it must be related.

Regards,
Tim.

Reply all
Reply to author
Forward
0 new messages