Re: Mutable objects as mapping keys

Tim Peters (tim@ksr.com)
Sun, 28 Mar 93 23:24:56 EST

> if I create a list a = [1, 2] and use it as a dictionary key, e.g. d =
> {a: 'foobar'}, and now I call a.append(3), then the dictionary object
> is in trouble: it has saved a *pointer* to a as the key, so the value
> of d has suddenly changed to {[1, 2, 3]: 'foobar'}. This is not what
> you want! (I think :-)

Well, _I_ can live with that <smile>. In fact, if I think of a
dictionary as mapping _objects_ to objects, this is the behavior I
expect, since a.append(3) doesn't change a's identity _as an object_.
It's only confusing if I think of a dictionary as mapping _values_ to
objects. Both views can be useful, & I suspect the best thing to do is
to compromise:

> ...
> In Python using the pointer as a hash value would be safe (since it
> can't change), but this would make lists almost useless as keys: after
>
> d = {[1, 2]: 2; [1, 2, 3]: 3}
>
> the test
>
> d.has_key([1, 2])
>
> would yield false, since the [1, 2] used here is a different instance
> than the [1, 2] used as key.

The same is true in Icon, where "dictionaries" are called "tables", and
can be indexed by anything (strings, lists, other tables, pieces of code,
... anything). E.g.,

kaos 36= cat t.icn
procedure main()
local t, x
t := table() # a table, mapping anything to anything
x := [1,2] # a list
t[x] := 5
if /t[x] then write( "x not in table" )
else write( "x associated with ", t[x] )
push( x, 3 ) # x becomes [1,2,3]
if /t[x] then write( "x not in table" )
else write( "x associated with ", t[x] )
x := [1,2,3] # same _value_ as x, but different object
if /t[x] then write( "x not in table" )
else write( "x associated with ", t[x] )
end
kaos 37= it -s t -x
x associated with 5
x associated with 5
x not in table
kaos 38=

This doesn't stop Icon's tables from being useful. E.g., the Icon
Program Library uses this feature many times, especially in routines to
provide printable representations for arbitrary (including cyclic) data
structures. In fact, "pointer equality" semantics for structured objects
are crucial to making that kind of routine easy to write. I can't
remember the specifics now, but I know that in Icon I once even had a
"good use" for a table that was itself indexed by tables (some hairy
graph algorithm ... and again it was crucial that the mapping not be
affected by changes _to_ the tables used as indices).

The best approach is probably a philosophical mix: use "value equality"
for an index with type string or number or Null, and "object equality"
for everything else (I guess Null fits in either branch). The
distinction here is best viewed as being between, say, "atomic objects"
(those like numbers & strings with no internally visible structure) and
"structured objects" (those, like lists & tuples, built out of simpler
types). Probably best if d[2] and d[2L] and d[2.0] mapped to different
things too (so that "value equality" is sensitive to type as well as to
value).

I think I just recast Icon's rules into Pythonese there. It's a pretty
good way to go in practice, & I really don't know of a better one.

Perl carries value equality to an extreme, so that after
$dict{1} = 'hi';
$dict{1} and $dict{'1'} and $dict{cos(0.0)} all refer to 'hi' too.
_That_ can be surprising.

thinking-this-is-one-of-those-happy-cases-where-the-easiest-implementation-
is-also-the-most-efficient-&-the-most-useful-ly y'rs - tim

Tim Peters tim@ksr.com
not speaking for Kendall Square Research Corp