default values for maps

11,327 views
Skip to first unread message

chris dollin

unread,
Mar 17, 2010, 7:38:24 AM3/17/10
to golang-nuts
Dear All

(Tried but failed to find previous mention of this topic
despite vague memory of having seen /something/.)

It was a source of some minor inconvenience to me that the
map lookup operation m[k] explodes if there is no entry
for k in m.

Taking defensive action for this requires either pulling the
map access out to the statement level to do the

  value, present := m[k] ; if present ...

trick, or replacing m[k] by something like mylookup(m, k)
and writing that function. (Or have I missed something?)

This was in a context where a clear "empty" value for the missing
key would make sense. (I would say that the type's zero value
would be the obvious candidate, but if I remember correctly
the zero value for maps isn't an empty map, and it happens
that in the code that prompted this message the map's value
type was itself a map being used to represent a set.)

It would be nice if there was a way to specify for a map the
value to be returned when a lookup was done with an absent
key, inclusive-or a function to run to deliver a value for an
absent key, inclusive-or a way of doing the lookup which
provided the value to use if the key was absent eg m[k,v],
inclusive-or an operator SPOO [1] such that

  (x, y) SPOO z

is x if y is true and z otherwise, where (x, y) is some bivalued
expression such as, say, m[k]. (SPOO sadly isn't powerful
enough to substitute for Go's missing conditional expression .)

If I had to pick one such operation it would be the second; attaching
to a map, preferably at creation time, a function which was called
to supply a value for a key not present in the map.
 
[1] Spelling to be chosen.

--
Chris "allusive" Dollin

Ostsol

unread,
Mar 17, 2010, 7:46:32 AM3/17/10
to golang-nuts
A map lookup also returns an optional boolean value [1], which
indicates whether the lookup was successful or not -- without blowing
up.

v, ok := stuff["thing"]

If there is no "thing" in stuff, ok is false.

[1] http://golang.org/doc/go_spec.html#Indexes

-Daniel

chris dollin

unread,
Mar 17, 2010, 7:58:23 AM3/17/10
to Ostsol, golang-nuts
On 17 March 2010 11:46, Ostsol <ost...@gmail.com> wrote:
A map lookup also returns an optional boolean value [1], which
indicates whether the lookup was successful or not -- without blowing
up.

v, ok := stuff["thing"]

If there is no "thing" in stuff, ok is false.

Yes, I know -- I explicitly referred to that option and gave
at least a hint of why it is unsatisfactory.

--
Chris "allusive" Dollin

yy

unread,
Mar 17, 2010, 8:40:42 AM3/17/10
to chris dollin, Ostsol, golang-nuts
2010/3/17 chris dollin <ehog....@googlemail.com>:

Yes, you referred to it. However, it is not clear for me why it is
unsatisfactory.

func mylookup(m map, k key, default interface{}) interface{} {
if v, present := m[k]; present {
return v
}
return default
}

This function does the same task that SPOO in your example and is
short and clean. Since default values have already been rejected for
explicitness, I don't think this (probably less useful for most of us)
case justifies a language change.

--
- yiyus || JGL . 4l77.com

chris dollin

unread,
Mar 17, 2010, 9:05:03 AM3/17/10
to yy, Ostsol, golang-nuts
On 17 March 2010 12:40, yy <yiyu...@gmail.com> wrote:
2010/3/17 chris dollin <ehog....@googlemail.com>:
> On 17 March 2010 11:46, Ostsol <ost...@gmail.com> wrote:
>>
>> A map lookup also returns an optional boolean value [1], which
>> indicates whether the lookup was successful or not -- without blowing
>> up.
>>
>> v, ok := stuff["thing"]
>>
>> If there is no "thing" in stuff, ok is false.
>
> Yes, I know -- I explicitly referred to that option and gave
> at least a hint of why it is unsatisfactory.
>

Yes, you referred to it. However, it is not clear for me why it is
unsatisfactory.

Because it's clumsy if the value is being used inside some
expression, rather than appearing naturally at statement level.

Consider as a tiny example a map from strings to counts of
how often they appear in some source. Contrast

  counts[s] += 1

where counts is a map with default value 0, with

  count, present := counts[s]
  counts[s] = count + 1

If the default value wasn't the zero value this would be even
more tedious:

  count, present := count[s]
  if present { counts[s] = count + 1 } else { count[s] = defaultPlus1 }

My view is that the plumbing is getting in the way of the
meaning. 
 
func mylookup(m map, k key, default interface{}) interface{} {
  if v, present := m[k]; present {
     return v
  }
  return default
}

This function does the same task that SPOO in your example

For some weak value of "the same".

(a) the default argument is always evaluated -- I see I didn't make
that clear in my original post. It matters.

(b) The type is wrong; it returns interface{}, not whatever the
actual value type is, and so will almost always need casting.
(Generics would fix this problem, of course.)

I can live with the current situation, of course. And I appreciate
that the Go authors don't want Go to suffer the death of one
thousand features. But, still, this case is clunky.

--
Chris "allusive" Dollin

Esko Luontola

unread,
Mar 17, 2010, 9:05:52 AM3/17/10
to golang-nuts
Also, it's useful that the map panics if the key is not found. If the
code expects to find the value from the map (by using the single-
assignment version), then it is a bug if the value is not in the map,
and in that case blowing up as soon as possible helps in noticing the
bug and getting it fixed.

chris dollin

unread,
Mar 17, 2010, 9:17:06 AM3/17/10
to Esko Luontola, golang-nuts
It's /sometimes/ useful. Note that I am not asking for the existing
behaviour be changed [1]; I'm suggesting the /option/ of giving maps
default values /for those times when it's convenient/.

The example I fell over, and the example I gave in my previous
message, were exactly cases where missing! panic! is unhelpful,
and having to avoid the panic makes the code clumsy.

[1] ie, maps that don't get defaults set remain panicky.

--
Chris "allusive" Dollin

yy

unread,
Mar 17, 2010, 10:00:26 AM3/17/10
to chris dollin, Ostsol, golang-nuts
2010/3/17 chris dollin <ehog....@googlemail.com>:

> Because it's clumsy if the value is being used inside some
> expression, rather than appearing naturally at statement level.
>

Well, I see your point but I don't know why maps should be considered
different to any other type. You cannot use undefined variables
neither (non-defined variables could also be automatically initialized
to zero, but they are not).

> Consider as a tiny example a map from strings to counts of
> how often they appear in some source. Contrast
>
>   counts[s] += 1
>
> where counts is a map with default value 0, with
>
>   count, present := counts[s]
>   counts[s] = count + 1
>
> If the default value wasn't the zero value this would be even
> more tedious:
>
>   count, present := count[s]
>   if present { counts[s] = count + 1 } else { count[s] = defaultPlus1 }
>

IMO in this particular example the best solution would be an explicit
count[s] = 0 before beginning to count. Or, if as it looks like you
are counting words, something like:

func countonemore(s string) {
if count[s], present; !present {
count[s] = 0
}
count[s]++
}

Is this such a problem as to require a language change?

>>
>> This function does the same task that SPOO in your example
>
> For some weak value of "the same".
>
> (a) the default argument is always evaluated -- I see I didn't make
> that clear in my original post. It matters.
>
> (b) The type is wrong; it returns interface{}, not whatever the
> actual value type is, and so will almost always need casting.
> (Generics would fix this problem, of course.)
>

(c) My function does not initialize the value for further use, it
should do m[k] = 0 before returning.

But my point is that a similar function to solve your particular
problem will not be clunkier than any other Go function.

> I can live with the current situation, of course. And I appreciate
> that the Go authors don't want Go to suffer the death of one
> thousand features. But, still, this case is clunky.
>

I think we disagree because we see maps in a different way. In my very
simplified view, map values are something like normal variables, but
with a runtime name. If you do this assumption, it is clear that a new
map value (count[s]) has to be initialized before being used, in the
same manner that you have to declare a count_s variable before
referring to it.

That said, I don't think default map values would be a bad thing. I
just think that writing the mylookup function is a better solution at
the end of the day, but that is only my personal opinion.

yy

unread,
Mar 17, 2010, 10:07:02 AM3/17/10
to chris dollin, Ostsol, golang-nuts
func countonemore(s string) {
  if _, present := count[s]; !present {

     count[s] = 0
  }
  count[s]++
}

Sorry
(also, notice that I'm assuming you can access count from this
function, but you could also pass it as a parameter or make the
function a method of a count type)

chris dollin

unread,
Mar 17, 2010, 10:20:34 AM3/17/10
to yy, Ostsol, golang-nuts
On 17 March 2010 14:00, yy <yiyu...@gmail.com> wrote:
2010/3/17 chris dollin <ehog....@googlemail.com>:
> Because it's clumsy if the value is being used inside some
> expression, rather than appearing naturally at statement level.
>

Well, I see your point but I don't know why maps should be considered different to any other type.

Maps /are/ different to other types. If they weren't, the other
types would be maps.
 
You cannot use undefined variables neither

What have variables to do with it? Variable-name lookup
is a compile-time operation in the context of a closed set of
names, to wit, those in scope. Map lookup is a run-time
operation in an open set of values.
 
(non-defined variables could also be automatically initialized
to zero, but they are not).

I don't see that as relevant.

> Consider as a tiny example a map from strings to counts of
> how often they appear in some source. Contrast

(fx:snip)

IMO in this particular example the best solution would be an explicit  count[s] = 0 before beginning to count.

The set of values of s is unknown in advance.

Or, if as it looks like you
are counting words, something like:

func countonemore(s string) {
  if count[s], present; !present {
     count[s] = 0
  }
  count[s]++
}

Horrible, isn't it. You really don't that's ... excessive? Opaque?
 
Is this such a problem as to require a language change?

As to /require/ one? That's not for me to say. It won't, on its own,
stop me from using -- or at least learning -- Go. It's just another
burr.

I think we disagree because we see maps in a different way. In my very simplified view, map values are something like normal variables, but with a runtime name.

If I were to adopt that view -- I'm not sure I would -- the maps
would be /collections/ of /related/ variables, with initial values
for all of them, even the ones that hadn't yet been mentioned.
 
If you do this assumption, it is clear that a newmap value (count[s]) has to be initialized before being used,

That's what all the options I suggested in my original post
do; they allow you to specify the initial value for an entry.
Some let you do so at the point of lookup, and some let you
do so "earlier", by attaching the value or a generator for it
to the map.

That said, I don't think default map values would be a bad thing. I just think that writing the mylookup function is a better solution at the end of the day, but that is only my personal opinion.

I would be happier (not happy, but happier) with the mylookup
function if it could be written once as a (typed!) generic. But
I think the discussion as served at least for me to identify my
preferred feature, even if I'm not going to get it -- attaching a
generator to the map. (Makes mental note for own language
design.)

Chris

--
Chris "allusive" Dollin

Rob 'Commander' Pike

unread,
Mar 17, 2010, 11:02:38 AM3/17/10
to chris dollin, golang-nuts
The simple reason is to be able to distinguish "absent" from "present with zero value".

-rob

chris dollin

unread,
Mar 17, 2010, 11:14:25 AM3/17/10
to Rob 'Commander' Pike, golang-nuts
On 17 March 2010 15:02, Rob 'Commander' Pike <r...@google.com> wrote:
The simple reason is to be able to distinguish "absent" from "present with zero value".

Nothing I've suggested would remove that ability.

(There's some fine detail around m[k] = defaultValue, though.)

--
Chris "allusive" Dollin

Eleanor McHugh

unread,
Mar 17, 2010, 1:18:52 PM3/17/10
to golang-nuts
On 17 Mar 2010, at 15:02, Rob 'Commander' Pike wrote:
> The simple reason is to be able to distinguish "absent" from "present with zero value".

The how about adding some symmetry by having arrays and slices return a (value, ok) couplet for out-of-bounds errors? Being able to apply the same coding patterns with both sets of containers would be a nice gain.

The idea of having a default value isn't such a bad one. Having to check whether an entry exists before using it makes sense for some programming problems, but for sparse arrays where the map represents distinct features against a generally featureless background this leads to unnecessary repetition.

Whilst adding a generator to a map may be somewhat more difficult, this too can be very handy when a map represents a diverse but statistically determined population where individuals can still be individually identified and subsequently tracked.

I'm not disputing that both features could be modelled as user types and functions used to provide the same functionality, just wondering whether we should consider maps a low-level storage mechanism or if the additional boilerplate that requires is really necessary?


Ellie

Eleanor McHugh
Games With Brains
http://feyeleanor.tel

Michael Hoisie

unread,
Mar 17, 2010, 1:33:45 PM3/17/10
to golang-nuts
Putting aside theory, key errors were the #1 reason that my Go web
applications crashed. In a web application you just can't make any
assumptions about which query parameters would be there.

So to fix that, my code became littered with:
var val string
if val, ok := params[name]; !ok {
// return an error message or something
}

That got to be ugly, so I just made a wrapper like this, which
returned a default string value ""

func (r *Request) GetParam(name string) string {
if r.Params == nil || len(r.Params) == 0 {
return ""
}
params, ok := r.Params[name]
if !ok || len(params) == 0 {
return ""
}
return params[0]
}

This actually simplified my code quite a bit, and eliminated crashes
due to Key errors. I guess it would be handy to have language support
for defaults, but you could always just write a wrapper.

I would, however, like to see more natural syntax for checking for
existence in a map.
if m[key] { ... }
or
if key in m {...}

Also, for removing a key from a map. The way it's done now is weird.
I'd like to have something like:
delete( m, key ) or delete m[key] or delete(m[key])

- Mike

On Mar 17, 10:18 am, Eleanor McHugh <elea...@games-with-brains.com>
wrote:

chris dollin

unread,
Mar 17, 2010, 1:42:04 PM3/17/10
to Michael Hoisie, golang-nuts
On 17 March 2010 17:33, Michael Hoisie <hoi...@gmail.com> wrote:

This actually simplified my code quite a bit, and eliminated crashes
due to Key errors. I guess it would be handy to have language support for defaults, but you could always just write a wrapper.

Either one wrapper for each type-pair you used for maps, or one
wrapper with an interface{}, or some intermediate mess ...
 
I would, however, like to see more natural syntax for checking for
existence in a map.
if m[key] { ... }
or
if key in m {...}

Concur.
 
Also, for removing a key from a map. The way it's done now is weird.
I'd like to have something like:
delete( m, key ) or delete m[key] or delete(m[key])

Concur, modulo not liking syntax that hints at m[key] getting
evaluated for value, and a preference for not introducing keywords.

I do get the impression the Go authors aren't too keen on adding
methods to the built-in types, but I would have thought both of these
would most straightforwardly be dealt with by adding methods to
maps

  m.Contains( key ) bool
  m.Remove( key ) possiblyReturningAnyExistingValueForKey
  m.RemoveAndPanicIfAbsent( key )

Perhaps the latter should have a shorter name.

--
Chris "allusive" Dollin

cthom lists

unread,
Mar 17, 2010, 1:49:48 PM3/17/10
to Eleanor McHugh, golang-nuts
On Wed, Mar 17, 2010 at 1:18 PM, Eleanor McHugh <ele...@games-with-brains.com> wrote:

The how about adding some symmetry by having arrays and slices return a (value, ok) couplet for out-of-bounds errors? Being able to apply the same coding patterns with both sets of containers would be a nice gain.


+1, I'm not much for adding tons of features but this one makes perfect sense to me. 

Rob 'Commander' Pike

unread,
Mar 17, 2010, 2:31:38 PM3/17/10
to cthom lists, Eleanor McHugh, golang-nuts
We talked about it and you're right that it would add some symmetry but we decided it wasn't worth it. It's so cheap and easy to check the index of a slice or array (vs. hard for a map) that it seemed simpler to leave it out.

-rob

Eleanor McHugh

unread,
Mar 17, 2010, 5:39:41 PM3/17/10
to golang-nuts

We can't fault you guys for making that choice as nearly every imperative language opts for manual array bounds checking or handling a runtime error/exception. My fave approach to this was Icon's, probably because it was my first serious use of an expression-based language and the goal-directed evaluation mechanism was so much fun to code with. I fake the same kind of thing in Ruby using nil but the absence of a cut operator still bugs me at times :)

Anyway, I'm not suggesting that Go should go anywhere near that sort of thing, just that having symmetry in the way the container types operate could simplify code maintenance and make life easier for people who want to support both dense and sparse arrays.


Ellie

Eleanor McHugh
Games With Brains
http://feyeleanor.tel

----
raise ArgumentError unless @reality.responds_to? :reason

roger peppe

unread,
Mar 18, 2010, 4:28:23 AM3/18/10
to Eleanor McHugh, golang-nuts
it occurs to me that it would be possible to add another
optional argument to map's make, allowing access
to elements not present.

the += case is my main motivating example.

i don't think that this would affect current behaviour.

e.g.
t := make(map[int]string, 0, true)
fmt.Println(t[99]) // ok; prints empty string.
t[12334] += "x" // also ok. t[12334] is now "x".
v, present := t[999] // v is ""; present is false.

alternatively, the zero element could be explicitly specified:

e.g.
t := make(map[int]string, 0, "zero")
fmt.Println(t[99]) // ok; prints "zero".
t[12334] += "x" // also ok. t[12334] is now "zerox".
v, present := t[999] // v is "zero"; present is false.

chris dollin

unread,
Mar 18, 2010, 5:14:52 AM3/18/10
to roger peppe, Eleanor McHugh, golang-nuts
On 18 March 2010 08:28, roger peppe <rogp...@gmail.com> wrote:
it occurs to me that it would be possible to add another
optional argument to map's make, allowing access
to elements not present.

If this were to be done, I believe that rather than supplying a
valueToUseIfAbsent we should supply a functionToCallIfAbsent
ToSupplyTheValue.

This covers the case where the vTUIA is different for each
entry, eg when the values in the map are themselves maps
or container.Vectors or whatever.

--
Chris "allusive" Dollin

Dan

unread,
Mar 18, 2010, 5:49:45 AM3/18/10
to golang-nuts
>
> > it occurs to me that it would be possible to add another
> > optional argument to map's make, allowing access
> > to elements not present.
>
> If this were to be done, I believe that rather than supplying a
> valueToUseIfAbsent we should supply a functionToCallIfAbsent
> ToSupplyTheValue.
>
> This covers the case where the vTUIA is different for each
> entry, eg when the values in the map are themselves maps
> or container.Vectors or whatever.
>

Coming from Java/C#/C++, the solution seems obvious: define your own
map type that has a default value, and another type that uses a
generator function to give you default values. Sure, it won't be as
nice to use as the one in the language, but hopefully generics will
improve that.

chris dollin

unread,
Mar 18, 2010, 6:10:25 AM3/18/10
to Dan, golang-nuts

I would be unwilling to make this kind of extension to maps hostage
to fortune in that way.

(Without generics, I think it would be pretty horrible; I remember
pre-generics Java.)

--
Chris "allusive" Dollin

Rob 'Commander' Pike

unread,
Mar 22, 2010, 7:10:50 PM3/22/10
to roger peppe, Eleanor McHugh, golang-nuts

A simpler and attractive approach is just to have m[x] return the zero
value if the lookup fails. If you need to check for absent nil vs.
present nil, you could still use ,ok.

-rob

chris dollin

unread,
Mar 23, 2010, 3:16:13 AM3/23/10
to Rob 'Commander' Pike, roger peppe, Eleanor McHugh, golang-nuts

It's certainly simpler, but substantially less attractive (to me; clearly
opinions may differ) since the zero value is sufficiently often not
useful -- anything for which make() is appropriate is something for
which returning the zero value from a default lookup is unhelpful,
and any pointer type.

--
Chris "allusive" Dollin

roger peppe

unread,
Mar 23, 2010, 5:29:29 AM3/23/10
to chris dollin, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
On 23 March 2010 07:16, chris dollin <ehog....@googlemail.com> wrote:
> It's certainly simpler, but substantially less attractive (to me; clearly
> opinions may differ) since the zero value is sufficiently often not
> useful -- anything for which make() is appropriate is something for
> which returning the zero value from a default lookup is unhelpful,
> and any pointer type.

i think you're trying to argue to be able to provide an arbitrary
element-creation function
but i don't think that would work well. in particular, it would seem wrong that

e := m[x]

should be equivalent to

e, ok := m[x]
if !ok {
e = makeElement()
m[x] = e
}

because then looking up an element would have
the side-effect of changing the map, a very
undesirable trait.

if you accept that, then the functionality becomes substantially less useful.

for instance, imagine a map containing struct pointers, with an
element creation function that allocates a new struct:

type T struct {x, y int}
m := make(map[string] *T, 0, func() *T { return new(T); })

it would seem logical to be able to do this:

e := m["foo"]
e.x = 999

but this will do nothing if "foo" was not already present.

also for comparable types, m["foo"] == m["foo"] would not
necessarily be true.

returning a zero value is useful precisely because the same
zero value is returned for every element, and it's a *value* - there
are no reference semantics implied, so we would not
expect this:

m := make(map[string] T) // note value element type.
e := m["foo"].x
e.x = 999

to have any side-effects on m.

m["foo"].x and &m["foo"] are already illegal, so there's no problem there.

i'd be happy to have the zero value returned when the lookup fails.

chris dollin

unread,
Mar 23, 2010, 5:44:09 AM3/23/10
to roger peppe, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
On 23 March 2010 09:29, roger peppe <rogp...@gmail.com> wrote:

On 23 March 2010 07:16, chris dollin <ehog....@googlemail.com> wrote:
> It's certainly simpler, but substantially less attractive (to me; clearly
> opinions may differ) since the zero value is sufficiently often not
> useful -- anything for which make() is appropriate is something for
> which returning the zero value from a default lookup is unhelpful,
> and any pointer type.

i think you're trying to argue to be able to provide an arbitrary
element-creation function

Yes -- both creation and, for want of a better word, attachment.
Without those you don't get the straightforward uses I was talking
about in my original post.
 
but i don't think that would work well. in particular, it would seem wrong that

 e := m[x]

should be equivalent to

 e, ok := m[x]
 if !ok {
     e = makeElement()
     m[x] = e
 }

because then looking up an element would have
the side-effect of changing the map, a very
undesirable trait.

Only for /those maps where the developer has explicitly
requested the feature/. For such a map, it's not undesirable -- it's
the point.

Another way of looking at it it so say such a map maps
every possible key to some value, but those values are not
materialised until the key is looked up.

if you accept that, then the functionality becomes substantially less useful.

for instance, imagine a map containing struct pointers, with an
element creation function that allocates a new struct:

  type T struct {x, y int}
  m := make(map[string] *T, 0, func() *T { return new(T); })

it would seem logical to be able to do this:

 e := m["foo"]
 e.x = 999

but this will do nothing if "foo" was not already present.

Which is a compelling argument for allowing the default
value mechanism to update the map. 

also for comparable types, m["foo"] == m["foo"] would not
necessarily be true.

We paint DON'T DO THAT in red in the documentation.
(Having the default mechanism automatically update the map would
prevent this form happening, which is an argument for making
that the defined behaviour rather than an option of the generator.)


i'd be happy to have the zero value returned when the lookup fails.

I think it's clean, straightforward, and inadequate.

--
Chris "allusive" Dollin

roger peppe

unread,
Mar 23, 2010, 5:55:44 AM3/23/10
to chris dollin, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
On 23 March 2010 09:44, chris dollin <ehog....@googlemail.com> wrote:
> Another way of looking at it it so say such a map maps
> every possible key to some value, but those values are not
> materialised until the key is looked up.

so would you make the following two statements have
different side effects on m?

e := m[x]


e, ok := m[x]

i think it would be confusing if they did.
and if they did not, then there would be no way to
test for an element's existence in the map.

>> i'd be happy to have the zero value returned when the lookup fails.
>
> I think it's clean, straightforward, and inadequate.

one could say that about the ways that the zero
value is used in many other places in Go, but it
seems to cope ok.

chris dollin

unread,
Mar 23, 2010, 6:06:02 AM3/23/10
to roger peppe, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
On 23 March 2010 09:55, roger peppe <rogp...@gmail.com> wrote:
On 23 March 2010 09:44, chris dollin <ehog....@googlemail.com> wrote:
> Another way of looking at it it so say such a map maps
> every possible key to some value, but those values are not
> materialised until the key is looked up.

so would you make the following two statements have
different side effects on m?

e := m[x]
e, ok := m[x]

Yes.

One is "give me that value that m has for x, materialising it
if necessary", and the other is "tell me if x is present as a key
in m, and the associated current value if any, zero if not."

Two different constructs with two different (but related) meanings.

This reading rather ruins my "another way" view, which would
need to make more explicit the difference between potential
and actual entries.
 
i think it would be confusing if they did.
and if they did not, then there would be no way to
test for an element's existence in the map.

I don't think it would be as confusing as you fear.


>> i'd be happy to have the zero value returned when the lookup fails.
>
> I think it's clean, straightforward, and inadequate.

one could say that about the ways that the zero
value is used in many other places in Go, but it
seems to cope ok.

In my view, that doesn't extend to /this/ place, for the reasons I started
with.

--
Chris "allusive" Dollin

roger peppe

unread,
Mar 23, 2010, 6:23:18 AM3/23/10
to chris dollin, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
i think that it would be a bad idea to change
the language such that a map index operation could
have arbitrary side effects.

in fact, i think it's generally a bad idea to make
an indexing operation have side effects at all.

regardless, this argument now appears to be academic:

http://codereview.appspot.com/673042/show

chris dollin

unread,
Mar 23, 2010, 6:42:05 AM3/23/10
to roger peppe, Rob 'Commander' Pike, Eleanor McHugh, golang-nuts
On 23 March 2010 10:23, roger peppe <rogp...@gmail.com> wrote:
i think that it would be a bad idea to change
the language such that a map index operation could
have arbitrary side effects.

While what I'm suggesting /could/ produce arbitrary side
effects, the intended use is much more constrained. Go
allows function calls in expressions to have arbitrary side
effects, even though that's sometimes a bad idea. Go allows
exported variables to be assigned to from anywhere, even
though that's sometimes (IMO, very often) a bad idea. An
argument against default value construction needs, I think,
to be stronger.

Clearly our positions differ.

in fact, i think it's generally a bad idea to make
an indexing operation have side effects at all.

Generally perhaps, but this is not a general case. 
 
regardless, this argument now appears to be academic:

http://codereview.appspot.com/673042/show

I don't think relaxing the rules so that m[notpresent] doesn't
explode makes this argument about non-zero default
arguments academic in the least.

But I don't think there's much else for me to say -- I've expressed
my positions and values as best I can, at least for now. I have
code that works -- it's just horrid -- and a much clearer idea of
exactly what I want, that I can put into other design activity.

--
Chris "generics would help" Dollin

Esko Luontola

unread,
Mar 23, 2010, 7:53:35 AM3/23/10
to golang-nuts
When the code is written following Composed Method [1], then each read
and write to a map will naturally become its own method, so even if
there is some boilerplate (such as casting from interface{} or
creating default values), that boilerplate will not be duplicated. So
the benefits from this language change would be very limited: For each
struct with a map member, instead of having one 3-5 line method, you
can have one 1 line method. But at the same time, the knowledge of
what is returned from a map is non-local, because it's not enough to
see just the code which reads from the map, but instead you must see
also how the map is created to know whether it will return some custom
default value.

http://www.infoq.com/presentations/10-Ways-to-Better-Code-Neal-Ford

chris dollin

unread,
Mar 23, 2010, 8:20:08 AM3/23/10
to Esko Luontola, golang-nuts
On 23 March 2010 11:53, Esko Luontola <esko.l...@gmail.com> wrote:
When the code is written following Composed Method [1],

Have you a link to some text rather than flash presentations?

then each read
and write to a map will naturally become its own method,

In that case, it seems unlikely to me that I'd use Composed Method.

so even if
there is some boilerplate (such as casting from interface{} or
creating default values), that boilerplate will not be duplicated. So
the benefits from this language change would be very limited: For each
struct with a map member, instead of having one 3-5 line method, you
can have one 1 line method.

The change I have suggested reduces the example I have
to hand:

func (x *SM) Epsilon( from State, toAll States ) {
  states, present := x.epsilons[from]
  if present == false {
      states = make(States)
      x.epsilons[from] = states
  }
  states.AddAll( toAll )
  for to, _ := range toAll {
      all, present := x.epsilons[to]
      if present { states.AddAll( all ) }
 }
}

to

func (x *SM) Epsilon( from State, toAll States ) {
  states := x.epsilons[from]
  states.AddAll( toAll )
  for to, _ := range toAll { states.AddAll( x.epsilons[to] ) }
}

reducing 8 lines to 3 (+ 1 for the attachment of the generator)
and in my opinion (and I know opinions differ) makes the code
much clearer.
 
But at the same time, the knowledge of
what is returned from a map is non-local, because it's not enough to
see just the code which reads from the map, but instead you must see
also how the map is created to know whether it will return some custom
default value.

Yes, just as you must know what the map is for in order to write the
code that modifies it. This is a map from single State objects to
sets of State objects; every State is mapped to a States. It just so
happens that items not looked (yet) at don't have their States materialised, and not every State will need to materialise an
associated States object (which happens to be a map, as is
unfortunately hinted at by the `make` appearing in Epsilon.) 

--
Chris "allusive" Dollin

roger peppe

unread,
Mar 23, 2010, 9:55:34 AM3/23/10
to chris dollin, Esko Luontola, golang-nuts
for your example, it seems to me that it shouldn't be too
much trouble to write a helper function, e.g.

func getStates(m map[State] States, s State) (states States) {
if states = m[s]; states == nil {
states = make(States)
m[s] = states
}
return
}

then the code in question becomes simple again:

func (x *SM) Epsilon( from State, toAll States ) {

states := getStates(x.epsilons, from)


states.AddAll( toAll )
for to, _ := range toAll {

states.AddAll( getStates(x.epsilons, to) )
}
}

how many different map types will you need to write something
like getStates for? not many, i'll bet.

if go ever gets generics, you'd be able to do something
like this. until then, i don't think the current situation is so bad.

func mapget(m map['k] 'v, key 'k, mk func() 'v) (val 'v) {
var ok bool
if val, ok = m[key]; !ok {
val = mk()
m[key] = val
}
return
}

chris dollin

unread,
Mar 23, 2010, 10:18:37 AM3/23/10
to roger peppe, Esko Luontola, golang-nuts
On 23 March 2010 13:55, roger peppe <rogp...@gmail.com> wrote:
for your example, it seems to me that it shouldn't be too
much trouble to write a helper function, e.g.

func getStates(m map[State] States, s State) (states States) {
       if states = m[s]; states == nil {
               states =  make(States)
               m[s] = states
       }
       return
}

then the code in question becomes simple again:

func (x *SM) Epsilon( from State, toAll States ) {
       states := getStates(x.epsilons, from)
       states.AddAll( toAll )
       for to, _ := range toAll {
               states.AddAll( getStates(x.epsilons, to) )
       }
}

I /could/ do that, yes. It seems to me a weak return on investment.
 
how many different map types will you need to write something
like getStates for? not many, i'll bet.

Only for most of them, I'd expect. The zero value will be handy for
numbers and maybe strings, but not maps, slices, pointers, or
channels.
 
if go ever gets generics,

That would be nice, but I'm writing code now (for some value of
"now").

Must stop for now ...

--
Chris "allusive" Dollin

Dan Berindei

unread,
Apr 3, 2010, 10:53:39 AM4/3/10
to golang-nuts
On 23 mar., 15:20, chris dollin <ehog.he...@googlemail.com> wrote:
>
> > But at the same time, the knowledge of
> > what is returned from a map is non-local, because it's not enough to
> > see just the code which reads from the map, but instead you must see
> > also how the map is created to know whether it will return some custom
> > default value.
>
> Yes, just as you must know what the map is for in order to write the
> code that modifies it. This is a map from single State objects to
> sets of State objects; every State is mapped to a States. It just so
> happens that items not looked (yet) at don't have their States materialised,
> and not every State will need to materialise an
> associated States object (which happens to be a map, as is
> unfortunately hinted at by the `make` appearing in Epsilon.)
>

Suppose I have a map with your generator suggestion and I have to
write a function that needs to know which elements of the map have
been explicitly initialized. I use the commaok pattern and everything
is great.
But then another coworker comes along and needs to do something with
the map. The default value works for him and he doesn't know about my
function, so he just reads a value from the map without realizing that
he just broke my code by adding a new entry.

So looking at how the map was defined didn't help me at all. In order
to keep my code working I have to ensure that everyone else working
with the map also uses the commaok pattern. I'm afraid that doesn't
qualify as "better" to me.

As to your specific case, I'd say working with maps of maps sucks in
just about every language I know. My preferred alternative is to work
with tuples/structs as map keys, but Go doesn't yet support using your
structs as map keys.

chris dollin

unread,
Apr 5, 2010, 11:35:21 AM4/5/10
to Dan Berindei, golang-nuts
On 3 April 2010 15:53, Dan Berindei <dan.be...@gmail.com> wrote:
On 23 mar., 15:20, chris dollin <ehog.he...@googlemail.com> wrote:
>
> > But at the same time, the knowledge of
> > what is returned from a map is non-local, because it's not enough to
> > see just the code which reads from the map, but instead you must see
> > also how the map is created to know whether it will return some custom
> > default value.
>
> Yes, just as you must know what the map is for in order to write the
> code that modifies it. This is a map from single State objects to
> sets of State objects; every State is mapped to a States. It just so
> happens that items not looked (yet) at don't have their States materialised,
> and not every State will need to materialise an
> associated States object (which happens to be a map, as is
> unfortunately hinted at by the `make` appearing in Epsilon.)
>

Suppose I have a map with your generator suggestion and I have to
write a function that needs to know which elements of the map have
been explicitly initialized. I use the commaok pattern and everything
is great.
 
But then another coworker comes along and needs to do something with
the map. The default value works for him and he doesn't

read the documentation or the updates or the big warning you
put in the code or the test cases that support it or listen to the
discussions that led to the new function?

know about my
function,

or that they should /know/ for any given map type whether it
matters
 
so he just reads a value from the map without realizing that
he just broke my code by adding a new entry.

So looking at how the map was defined didn't help me at all. In order
to keep my code working I have to ensure that everyone else working
with the map also uses the commaok pattern. I'm afraid that doesn't
qualify as "better" to me.

What you descibe to me sounds like a more general process failure
then the technical details about default values.

I appreciate the hypothetical, but I don't debelie it would happen
uncaught often enough to be a problem.

As to your specific case, I'd say working with maps of maps sucks in
just about every language I know. My preferred alternative is to work
with tuples/structs as map keys, but Go doesn't yet support using your
structs as map keys.

Concur.

--
Chris "allusive" Dollin

Dan Berindei

unread,
Apr 5, 2010, 12:54:34 PM4/5/10
to chris dollin, golang-nuts
On Mon, Apr 5, 2010 at 6:35 PM, chris dollin <ehog....@googlemail.com> wrote:
> On 3 April 2010 15:53, Dan Berindei <dan.be...@gmail.com> wrote:
>>
>>
>> But then another coworker comes along and needs to do something with
>> the map. The default value works for him and he doesn't
>
> read the documentation or the updates or the big warning you
> put in the code or the test cases that support it or listen to the
> discussions that led to the new function?
>

Extra documentation and extra big warning signs seem like a poor
alternative to four extra lines of code for a helper function.

>> know about my
>> function,
>
> or that they should /know/ for any given map type whether it
> matters
>

A map is the same in every programming language that I know of: you
put stuff in it and you read it back. Making maps more complicated in
Go means it's harder for people coming from other programming
languages to learn the language and their code base.

>>
>> so he just reads a value from the map without realizing that
>> he just broke my code by adding a new entry.
>>
>> So looking at how the map was defined didn't help me at all. In order
>> to keep my code working I have to ensure that everyone else working
>> with the map also uses the commaok pattern. I'm afraid that doesn't
>> qualify as "better" to me.
>
> What you descibe to me sounds like a more general process failure
> then the technical details about default values.
>
> I appreciate the hypothetical, but I don't debelie it would happen
> uncaught often enough to be a problem.

On that line of reasoning I'd say custom defaults for maps don't
happen often enough to need language support. Especially when it has a
perfectly good workaround and it's not 100% risk free.

chris dollin

unread,
Apr 5, 2010, 1:25:38 PM4/5/10
to Dan Berindei, golang-nuts
On 5 April 2010 17:54, Dan Berindei <dan.be...@gmail.com> wrote:


A map is the same in every programming language that I know of: you
put stuff in it and you read it back.

Pop11's maps -- which it calls properties -- have variants which
allow a procedure to be called when the key is absent. From the
dates on the HELP page it looks like this feature was added not
later than 1992.

(Pop11 is an outlier along many axes.)

--
Chris "newanyproperty" Dollin

Eleanor McHugh

unread,
Apr 5, 2010, 4:32:25 PM4/5/10
to golang-nuts
On 5 Apr 2010, at 18:25, chris dollin wrote:
> On 5 April 2010 17:54, Dan Berindei <dan.be...@gmail.com> wrote:
> A map is the same in every programming language that I know of: you
> put stuff in it and you read it back.
>
> Pop11's maps -- which it calls properties -- have variants which
> allow a procedure to be called when the key is absent. From the
> dates on the HELP page it looks like this feature was added not
> later than 1992.

I recall it working that way back in 1990 when I was heavily into Pop11 (a breath of fresh air compared to the Fortran and Modula-2 I was lumbered with for college work).

In Ruby when a hash is created it can be given a block (anonymous closure) which is used to generate default values. Most times I recall encountering this usage in the wild have been related to sparse arrays or procedural memoisation of some kind.

> (Pop11 is an outlier along many axes.)

Pop11 was years ahead of its time, much like Icon and Snobol :)

Giles Lean

unread,
Apr 5, 2010, 9:59:07 PM4/5/10
to Dan Berindei, chris dollin, golang-nuts

Dan Berindei <dan.be...@gmail.com> wrote:

> Extra documentation and extra big warning signs seem like a
> poor alternative to four extra lines of code for a helper
> function.

> [ chop ]

> A map is the same in every programming language that I know
> of: you put stuff in it and you read it back. Making maps
> more complicated in Go means it's harder for people coming
> from other programming languages to learn the language and
> their code base.

What about programmers coming from languages where a zero
value is the expected result for non-existent values? My code
using the "comma ok" idiom still seems to compile, and it's
not adding values to the maps concerned.

It seems a bit of a half way house to /not/ create the entry
in the map when the comma ok idiom isn't used: IMHO I'd have
created the entry unless the comma, ok idiom was used.

Of course, IMHO I'm not sure MHO is particularly in favour
in some quarters, but now is the time to refine the new
behaviour if we're going to, so in summary I'd like to see
and am asking for:

o the "comma, ok" idom continue to work (I don't mind that
a zero value is returned along with ok == false for
non-existent entries; irrelevant)

o where "comma, ok" isn't used, then I'd like the map entry
created.

Getting a value out of a map for a key that isn't in the map
when you later use range on the map is just too weird for my
taste. (If there is precedent in other languages I'd like
to hear about it; perl is the scripting language I know
best, but I don't care to be bigoted about it.)

Now, I'm late enough for the appointment I wanted to be late
to, so it's time to move into the rather gray looking
outdoors.

Cheers,

Giles

Dan Berindei

unread,
Apr 7, 2010, 4:24:37 AM4/7/10
to Giles Lean, chris dollin, golang-nuts
On Tue, Apr 6, 2010 at 4:59 AM, Giles Lean <giles...@pobox.com> wrote:
>
> Dan Berindei <dan.be...@gmail.com> wrote:
>
>> Extra documentation and extra big warning signs seem like a
>> poor alternative to four extra lines of code for a helper
>> function.
>
>> [ chop ]
>
>> A map is the same in every programming language that I know
>> of: you put stuff in it and you read it back. Making maps
>> more complicated in Go means it's harder for people coming
>> from other programming languages to learn the language and
>> their code base.
>
> What about programmers coming from languages where a zero
> value is the expected result for non-existent values?  My code
> using the "comma ok" idiom still seems to compile, and it's
> not adding values to the maps concerned.
>

map[key] already gives you a zero value, so programmers coming from
such languages won't have any trouble with Go's status quo.
I don't see why they would expect 'map[key]' to behave like 'map[key]
= nil', however.

> It seems a bit of a half way house to /not/ create the entry
> in the map when the comma ok idiom isn't used: IMHO I'd have
> created the entry unless the comma, ok idiom was used.
>

Creating the entry automatically is IMHO even more half way: you have
a way to automatically insert the key but you don't have a way to know
if the key was inserted now or it was already in the map.

Giles Lean

unread,
Apr 7, 2010, 6:29:55 AM4/7/10
to Dan Berindei, chris dollin, golang-nuts

Dan Berindei <dan.be...@gmail.com> wrote:

> map[key] already gives you a zero value, so programmers coming from
> such languages won't have any trouble with Go's status quo.
> I don't see why they would expect 'map[key]' to behave like 'map[key]
> = nil', however.

Darn. This is about the third time in as many days as I've
goofed on this list. I was _sure_ that was what perl at least
did, and it doesn't.

What it does is give an undefined value, which in most cases
will be treated as "" or zero, and doesn't add anything to the
map (or hash, in perl's case).

Thanks for straightening me out, and my apologies to everyone
for (once more) wasting your time and potentially causing
confusion.

Giles (who promises to pause, check, recheck, and then post)

Reply all
Reply to author
Forward
0 new messages