nil map and delete

540 views
Skip to first unread message

Stefan Nilsson

unread,
Jan 1, 2013, 12:01:05 PM1/1/13
to golan...@googlegroups.com
Here is a nice feature that some of you might have missed. Without having to check for nil, you can delay the creation of a map until you actually need to add your first element. Take a look at this code:

var m map[int]int = nil

_ = len(m)
for _ = range m {
}
_ = m[0]
delete(m, 0)
m[0] = 0

In Go 1.0.3 the first three statements execute, but you get a panic when you call delete. At tip it's even better: the first four statements execute and only the attempted assignment fails.

I would love to see one more feature:

v, ok := delete(m, 0)

where v and ok would get the same values as produced by the assignment

v, ok := m[0]

I believe this could be added to the language without breaking any existing code. What's you thoughts on this?

minux

unread,
Jan 1, 2013, 12:14:19 PM1/1/13
to Stefan Nilsson, golan...@googlegroups.com
except that this new delete form is somewhat confusing.
why does the delete operation return values?

we'd better call it pop(m, 0) or something.

comparing to this suggestion, i think automatically making a new map at the first assignment might
be a better alternative, however, i don't think it fits Go's philosophy.

Dustin Sallings

unread,
Jan 1, 2013, 3:25:52 PM1/1/13
to golan...@googlegroups.com
minux <minu...@gmail.com> writes:

> except that this new delete form is somewhat confusing.
> why does the delete operation return values?
>
> we'd better call it pop(m, 0) or something.

Because knowing that the delete had an effect on a single pass is a
good thing.

I ran into the same issue where I had to do a map lookup to tell
whether or not it was there and then call delete to delete it so my API
(which predates go) could return whether or not the delete had an
effect.

I don't necessarily care about the old value, but it's quite common
and useful for delete operations to indicate whether they actually did
something.

--
dustin

David DENG

unread,
Jan 1, 2013, 11:07:13 PM1/1/13
to golan...@googlegroups.com
The problem may be the statement:

    m[0] = 1

is not likely to change the value of m itself, but the data pointed by the pointer (value) of m. So it is not straight to allocate the memory for m at this time, unless assigning an element to a map is like this:

    m = append(m, 0, 1)

which is very strange for a map.

I don't know what the perfect solution is.

David

Kevin Gillette

unread,
Jan 5, 2013, 3:09:52 AM1/5/13
to golan...@googlegroups.com
I don't think it's important to have a way to explicitly indicate that a delete/write and a read to occur in a single pass. Since Go maps have no concurrent-safety guarantees (you must mediate concurrent (r)w access via a mutex or channels), there's no semantic difference between a set of two statements that do a read then a write, and just one statement that does both, and it's the compiler's job to merge two identical consecutive lookups into one.

Stefan Nilsson

unread,
Jan 5, 2013, 5:37:15 AM1/5/13
to golan...@googlegroups.com
Absolutely, it's nothing essential, just a WIBNI ("Wouldn't it be nice if..."). But it would simplify code a bit:

if v, ok := delete(Expression1, Expression2); ok {
log.Printf("Removed %v", v)
}

Right now you need to write something like this:

{
m := Expression1
k := Expression2
v, ok := m[k]
delete(m, k)
if ok {
log.Printf("Removed %v", v)
}
}

Dave Cheney

unread,
Jan 5, 2013, 5:59:55 AM1/5/13
to Stefan Nilsson, golan...@googlegroups.com
Can you give a real world example where this would be needed ?
> --
>
>

Stefan Nilsson

unread,
Jan 5, 2013, 7:43:10 AM1/5/13
to golan...@googlegroups.com, Stefan Nilsson
The "real world" is a slippery concept, but in my case I was working on some graph algorithms based on this data structure:

// The map edges[v] contains the mapping {w:x} if there is an edge
// from v to w; x is the label assigned to this edge.
// The maps may be nil and are allocated only when needed.
edges []map[int]interface{}

Many of the maps will typically be nil and both the map and the key is often given by a nontrivial expression. When reviewing the code, I found some nil pointer dereferences and was surprised that my test code hadn't caught them. The truth was that Go allows quite a few operations on nil maps and my code was perfectly correct. I ended up removing lots of superfluous nil-pointer checks. The only remaining minor blemish was the delete statement.

Kevin Malachowski

unread,
Jan 5, 2013, 11:47:13 AM1/5/13
to golan...@googlegroups.com


On Saturday, 5 January 2013 05:37:15 UTC-5, Stefan Nilsson wrote:
Absolutely, it's nothing essential, just a WIBNI ("Wouldn't it be nice if..."). But it would simplify code a bit:

if v, ok := delete(Expression1, Expression2); ok {
log.Printf("Removed %v", v)
}

Right now you need to write something like this:

{
m := Expression1
k := Expression2
v, ok := m[k]
delete(m, k)
if ok {
log.Printf("Removed %v", v)
}
}

You still need to write some boiler-plate, but it's not actually that bad. Here's a simplified version:

{
m, k := Expression1, Expression2
if v, ok := m[k]; ok {
delete(m, k)
log.Printf("Removed %v", v)
}
}

Because, of course, you only need to attempt to delete from the map if the key exists in your map. It's really only two extra lines and from the point of view of "try not to do too much in a single line" I much prefer it this way.

Dustin Sallings

unread,
Jan 5, 2013, 4:38:36 PM1/5/13
to golan...@googlegroups.com
Dave Cheney <da...@cheney.net> writes:

> Can you give a real world example where this would be needed ?

Here's one:

https://github.com/couchbaselabs/cbgb/blob/478e145aed525e5e6a75db1af0d9a8c72fc5611f/vbucket.go#L145

The protocol specifies the behavior on a delete with or without the
value being present, so I have to handle the delete in both cases and
return which one I did.

This is an older version of the code, though. We eventually stopped
using maps, but before that we, supported conditional delete which
required us to know the value anyway. Though the "oldvalue,ok" response
would allow us to optimistically do conditional deletes, which would've
been neat.

--
dustin

Dustin Sallings

unread,
Jan 5, 2013, 4:41:12 PM1/5/13
to golan...@googlegroups.com
Kevin Malachowski <nifta...@gmail.com>
writes:

> Because, of course, you only need to attempt to delete from the map if
> the key exists in your map. It's really only two extra lines and from
> the point of view of "try not to do too much in a single line" I much
> prefer it this way.

That's two hash computations and hash table seeks when all you want to
know is whether it changed something (and perhaps what it changed).

It has to find that out to perform the delete in either case, so doing
it all in one step when you have the intention to delete wouldn't just
be less code, it'd be less time.

--
dustin

Dave Cheney

unread,
Jan 5, 2013, 4:48:09 PM1/5/13
to Dustin Sallings, golan...@googlegroups.com
You make a good point. I was originally going to say you were sweating
over one additional line, but the argument for the overhead of two
hash lookups is valid, but probably quite small compared the network
time of sending the 'deleted, but wasn't there in the first place'
message.
> --
>
>

Dustin Sallings

unread,
Jan 5, 2013, 4:53:54 PM1/5/13
to golan...@googlegroups.com
Dave Cheney <da...@cheney.net> writes:

> You make a good point. I was originally going to say you were sweating
> over one additional line, but the argument for the overhead of two
> hash lookups is valid, but probably quite small compared the network
> time of sending the 'deleted, but wasn't there in the first place'
> message.

Probably true, but the most common case is deleting things that were
there. So in the common path, I have to make two passes. Only in the
uncommon paths do I get to short-circuit it. So it's faster when it's
less likely. :)

You could imagine a similar thing being used as an assertion. If my
program is correct, I will only delete things that I have actually
added. So I can write delete like the following with no measurable
performance impact:

if !delete(m, key) {
panic(fmt.Errorf("Deletion of missing key %v", key))
}

--
dustin

Stefan Nilsson

unread,
Jan 5, 2013, 4:55:14 PM1/5/13
to golan...@googlegroups.com, clba...@gmail.com
The are two reasons I don't want to allocate all the maps up front in this case.  It would be wasteful to allocate lots of maps (hash tables) that are never used and it's possible to give better space estimates for the maps when they are actually needed. Being able to do lazy initialization is sometimes helpful and not having to worry much about the nil value is always nice.

I believe this is one of the reasons why the language designers chose to include a nil map and explicit map allocation instead of letting the zero value for a map be the empty map (just like the zero value for a string is the empty string).

Den lördagen den 5:e januari 2013 kl. 18:04:31 UTC+1 skrev clba...@gmail.com:
If you declare 'm' as:
        m := make(map[int]int,0)

rather than:
        var m map[int]int = nil

everything executes as expected without any change in delete or assignment expressions even with r1.0.x.

Johann Höchtl

unread,
Jan 5, 2013, 5:42:41 PM1/5/13
to golan...@googlegroups.com
Speculating: A smart(er) compiler should / will be able to optimize that superfluous second lookup away and reuse the first one.

Johann Höchtl

unread,
Jan 5, 2013, 5:42:43 PM1/5/13
to golan...@googlegroups.com

Kevin Gillette

unread,
Jan 5, 2013, 9:27:48 PM1/5/13
to golan...@googlegroups.com
In that case, I'd be worried that the read capability of delete would be used, as in:

if v, ok := delete(Expression1, Expression2); ok && shouldEdit(v) {
// we actually mean to modify
Expression1[Expression2] = modifyValue(v)
}

In this case, one combo read/delete followed by a write could occur, instead of a read followed by either a delete or a write -- the 'clever'/abused case is worse than the naive case that you're forced to do now. And yes, it is well within the realm of existing compiler technology to identify and group data accesses.

Russ Cox

unread,
Jan 6, 2013, 8:06:14 PM1/6/13
to clba...@gmail.com, golang-nuts
We considered various options for delete and decided to make it a
statement, not an expression, just like an assignment is a statement,
not an expression. And just as an assignment does not return what the
old value was, delete does not return what the old value was.

If you need the old value, it's quite easy to read from the map first.

Russ
Reply all
Reply to author
Forward
0 new messages