map without default values

162 views
Skip to first unread message

Joe Marty

unread,
Aug 13, 2020, 2:36:51 PM8/13/20
to golang-nuts
I'm very new to Go - apologies in advance if I'm missing something:

I find it frustrating that there's no way to create a map that does *not* automatically return a zero value for undefined key access by default.

I love the fact that Go doesn't return "nil" for this use case (I love Ruby, but I don't think they got that quite right), but returning a default value is worse!  It would make so much more sense if it would just panic when accessing an undefined key.  I'm not a Python guy, but I think this is something Python got right.  

Creating a map that returns some default value when you access an undefined key should be a special kind of map, or a special argument to initializing the map (which Ruby does allow).  In Go, is there even any way to create a map that panics when you access an undefined key?

-Joe

Connect with us here: LinkedInFacebookInstagramTwitterBuiltInAustin

https://www.smartersorting.com/

Axel Wagner

unread,
Aug 13, 2020, 2:42:39 PM8/13/20
to Joe Marty, golang-nuts
No, there isn't, but you can check if the value exists:

x, ok := m[k]
if !ok {
    panic("does not exist")
}

You can also wrap that with methods, if you want to avoid the extra check.

I disagree with you that panicking on a non-existent key is better - either in general, or in the majority of cases. Personally, I don't think I encountered many use-cases where the zero-value wasn't the perfect thing to use. I'm not saying your use-case doesn't exist or even that it's rare, just that I don't think you can generalize from your experience here.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/f53377a0-ddc1-4842-ac7a-a796d85b7077n%40googlegroups.com.

Michael Jones

unread,
Aug 13, 2020, 3:11:47 PM8/13/20
to Axel Wagner, Joe Marty, golang-nuts
Joe, your question is perfectly answered by Axel. 

I'll just share a few (personal) Go "style" comments:

Go likes you to test explicitly for failure where that is possible: did the open fail, did the pipe break, etc.Multiple return values make this clear by avoiding the need for a "reserved" error value of the normal-case return.

Go likes untested actions to work. In your case adding an existing key to a map replaces the old entry, accessing a missing entry returns the default value. ["no surprises"]

Go likes you to combine these approaches to make your own more elaborate behaviors using the "comma OK" approach that Axel shared. In your case, adding, and deleting could complain if there is already a matching entry, or not a matching entry, or -- and this is the real reason -- by looking at the payload of a new or matching entry to use application logic to decide if that's ok or not. Only you can know so Go won't second guess you, and the test-rather-than-panic style is because testing right there is deemed the right way.



--
Michael T. Jones
michae...@gmail.com

Joe Marty

unread,
Aug 14, 2020, 12:18:04 PM8/14/20
to golang-nuts
Thanks for the insights!

Yeah, I can see how the convention of "always testing for failure" is better than a panic, in that it requires you to handle the possibility of a failure for things that may commonly fail.  In that respect though, I don't understand why the "comma OK" is optional.  Elm, for instance, handles things similarly, and has no panics or errors at all as a result because it enforces handling of every possible outcome.  But in Go, it seems like... in places where it's not easy to use "comma OK" (maybe in the middle of a larger expression), or if I forget, I could easily confuse the default result from a boolean map (false) with an actual false value!  In Python or Elm, or Ruby, there would be a clear distinction and/or a panic.

I agree that it's not a common case that you might want a panic - in fact I *never* want a panic, haha!  I suppose it's more of a question of language convention vs. language enforcement.  Because I'm human, and rarely write perfect code, I would prefer for the compiler (or the runtime environment) to have an error if I make a mistake like that.  And even if in 90% of cases, it's not a mistake and can be resolved by using a sensible default value, if in 10% of cases it IS a mistake, I feel like it makes more sense that the language should enforce handling it, while making it easy to specify a default for the 90% of cases where one is actually appropriate.  In the current situation, there's no way for me to guard against mistakes in code maintenance.  I can intentionally panic in a particular instance, but if I'm maintaining the code a year later, and make a mistaken assumption about the existence of a key, intentionally checking and panicking in that case isn't really an option/solution since the definition of the context is that it's unintentional :D

I suppose maybe there's a way to setup a style linter to enforce a failure check for every map access?  (But that's a lot more tedious than just enabling a default where appropriate, or disabling it where it's not...)

On Thursday, August 13, 2020 at 2:11:47 PM UTC-5 Michael Jones wrote:
Joe, your question is perfectly answered by Axel. 

I'll just share a few (personal) Go "style" comments:

Go likes you to test explicitly for failure where that is possible: did the open fail, did the pipe break, etc.Multiple return values make this clear by avoiding the need for a "reserved" error value of the normal-case return.

Go likes untested actions to work. In your case adding an existing key to a map replaces the old entry, accessing a missing entry returns the default value. ["no surprises"]

Go likes you to combine these approaches to make your own more elaborate behaviors using the "comma OK" approach that Axel shared. In your case, adding, and deleting could complain if there is already a matching entry, or not a matching entry, or -- and this is the real reason -- by looking at the payload of a new or matching entry to use application logic to decide if that's ok or not. Only you can know so Go won't second guess you, and the test-rather-than-panic style is because testing right there is deemed the right way.

On Thu, Aug 13, 2020 at 11:42 AM 'Axel Wagner' via golang-nuts <golan...@googlegroups.com> wrote:
No, there isn't, but you can check if the value exists:

x, ok := m[k]
if !ok {
    panic("does not exist")
}

You can also wrap that with methods, if you want to avoid the extra check.

I disagree with you that panicking on a non-existent key is better - either in general, or in the majority of cases. Personally, I don't think I encountered many use-cases where the zero-value wasn't the perfect thing to use. I'm not saying your use-case doesn't exist or even that it's rare, just that I don't think you can generalize from your experience here.

On Thu, Aug 13, 2020 at 8:36 PM Joe Marty <joe....@smartersorting.com> wrote:
I'm very new to Go - apologies in advance if I'm missing something:

I find it frustrating that there's no way to create a map that does *not* automatically return a zero value for undefined key access by default.

I love the fact that Go doesn't return "nil" for this use case (I love Ruby, but I don't think they got that quite right), but returning a default value is worse!  It would make so much more sense if it would just panic when accessing an undefined key.  I'm not a Python guy, but I think this is something Python got right.  

Creating a map that returns some default value when you access an undefined key should be a special kind of map, or a special argument to initializing the map (which Ruby does allow).  In Go, is there even any way to create a map that panics when you access an undefined key?

-Joe

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/f53377a0-ddc1-4842-ac7a-a796d85b7077n%40googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Aug 14, 2020, 1:31:55 PM8/14/20
to Joe Marty, golang-nuts
On Fri, Aug 14, 2020 at 6:17 PM Joe Marty <joe....@smartersorting.com> wrote:
Yeah, I can see how the convention of "always testing for failure" is better than a panic, in that it requires you to handle the possibility of a failure for things that may commonly fail.  In that respect though, I don't understand why the "comma OK" is optional.

Because you want index-accesses as an expression with a single value (say, when passing it as a function argument). If I know that a value exists, or am fine using the zero value (again, that's the majority of my personal use-cases for maps at least), having to use a statement just to discard the extra bool is annoying.
 
Elm, for instance, handles things similarly, and has no panics or errors at all as a result because it enforces handling of every possible outcome.  But in Go, it seems like... in places where it's not easy to use "comma OK" (maybe in the middle of a larger expression), or if I forget, I could easily confuse the default result from a boolean map (false) with an actual false value!

FWIW, using `map[K]bool` as a set is a great example for where this behavior is super convenient. Far more convenient than if you use `map[K]struct{}`, which has significant syntactical overhead.

In Python or Elm, or Ruby, there would be a clear distinction and/or a panic. 

Well, an exception-based language of course is less hesitant to throw exceptions. But as a result, they often get ignored and cause crashes with useless stack traces (useless to the user, that is - the developer might find them more useful, but as a user, I want to know what I did wrong *without* having to look at source code).

Throwing an exception is a fine approach, if your language works that way. But "other languages are doing it" isn't a super good argument for doing the same - if it was, we only had one language.

I agree that it's not a common case that you might want a panic - in fact I *never* want a panic, haha!  I suppose it's more of a question of language convention vs. language enforcement.  Because I'm human, and rarely write perfect code, I would prefer for the compiler (or the runtime environment) to have an error if I make a mistake like that. 

There's a tradeoff going on, between readability (and writability) and safety. Requiring people to litter code with extra statements or _-assignments might make things a little bit safer, but it also makes code a little less readable.

Which is not to say that other tradeoffs aren't valid as well, but this is the one Go chose.

I suppose maybe there's a way to setup a style linter to enforce a failure check for every map access?  (But that's a lot more tedious than just enabling a default where appropriate, or disabling it where it's not...)

It's something you could do for your code base, for sure. But personally, I'd consider doing that bad style. Only check for membership, if membership is actually important.
 

Joe Marty

unread,
Aug 14, 2020, 2:52:20 PM8/14/20
to golang-nuts

If I know that a value exists, or am fine using the zero value (again, that's the majority of my personal use-cases for maps at least), having to use a statement just to discard the extra bool is annoying.
  
Right, so this brings me back to a nice solution to both our use cases, which would be to initialize the map as either having a default or not having a default.  You don't have to check for failure, I don't have to worry about forgetting to check for failure... right? :D

FWIW, using `map[K]bool` as a set is a great example for where this behavior is super convenient. Far more convenient than if you use `map[K]struct{}`, which has significant syntactical overhead.

Sure, that's a bad alternative.  So again, back to my original theory, seems like an overall better option to have `map[K]bool(false)` if you want the default to be false vs `map[K]bool` if you don't.  Or perhaps `openMap[K]bool` if you want every key to return something vs `closedMap[K]bool` if you don't.
 
Well, an exception-based language of course is less hesitant to throw exceptions. But as a result, they often get ignored and cause crashes with useless stack traces (useless to the user, that is - the developer might find them more useful, but as a user, I want to know what I did wrong *without* having to look at source code).
 
Fair point :)

I suppose maybe there's a way to setup a style linter to enforce a failure check for every map access?  (But that's a lot more tedious than just enabling a default where appropriate, or disabling it where it's not...)

It's something you could do for your code base, for sure. But personally, I'd consider doing that bad style. Only check for membership, if membership is actually important.

Well I definitely agree there about the ideal state - I should only have to check for membership when membership is important ideally.  So wouldn't it also be ideal to allow the developer to indicate when membership is important vs when it's not upon initialization, so that the runtime can panic when panic is appropriate, and just move along when "there's nothing to see here" - all without requiring any convention or ongoing discernment from maintainers of a code-base?

(And maybe not even panic, but just require checking for failure in one case, but not in the other, so that mistakes can be caught at compile time!) :D







Axel Wagner

unread,
Aug 14, 2020, 2:57:18 PM8/14/20
to Joe Marty, golang-nuts
On Fri, Aug 14, 2020 at 8:52 PM Joe Marty <joe....@smartersorting.com> wrote:

If I know that a value exists, or am fine using the zero value (again, that's the majority of my personal use-cases for maps at least), having to use a statement just to discard the extra bool is annoying.
  
Right, so this brings me back to a nice solution to both our use cases, which would be to initialize the map as either having a default or not having a default.  You don't have to check for failure, I don't have to worry about forgetting to check for failure... right? :D

Oh, sorry, I didn't catch that. The problem with that is that this is a pretty significant behavioral difference, so it at least would need to be reflected in the type (as you clarify). Having two different map types seems overkill to me.

It would definitely be possible. And if the problem was large enough, I might support the idea. But I don't feel it is, one way or another. Like, I would rather have a map-type which always panics, than have two different ones.
 

FWIW, using `map[K]bool` as a set is a great example for where this behavior is super convenient. Far more convenient than if you use `map[K]struct{}`, which has significant syntactical overhead.

Sure, that's a bad alternative.  So again, back to my original theory, seems like an overall better option to have `map[K]bool(false)` if you want the default to be false vs `map[K]bool` if you don't.  Or perhaps `openMap[K]bool` if you want every key to return something vs `closedMap[K]bool` if you don't.
 
Well, an exception-based language of course is less hesitant to throw exceptions. But as a result, they often get ignored and cause crashes with useless stack traces (useless to the user, that is - the developer might find them more useful, but as a user, I want to know what I did wrong *without* having to look at source code).
 
Fair point :)

I suppose maybe there's a way to setup a style linter to enforce a failure check for every map access?  (But that's a lot more tedious than just enabling a default where appropriate, or disabling it where it's not...)

It's something you could do for your code base, for sure. But personally, I'd consider doing that bad style. Only check for membership, if membership is actually important.

Well I definitely agree there about the ideal state - I should only have to check for membership when membership is important ideally.  So wouldn't it also be ideal to allow the developer to indicate when membership is important vs when it's not upon initialization, so that the runtime can panic when panic is appropriate, and just move along when "there's nothing to see here" - all without requiring any convention or ongoing discernment from maintainers of a code-base?

(And maybe not even panic, but just require checking for failure in one case, but not in the other, so that mistakes can be caught at compile time!) :D








Connect with us here: LinkedInFacebookInstagramTwitterBuiltInAustin

https://www.smartersorting.com/

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

jake...@gmail.com

unread,
Aug 15, 2020, 12:00:32 PM8/15/20
to golang-nuts
On Friday, August 14, 2020 at 2:52:20 PM UTC-4 Joe Marty wrote:

If I know that a value exists, or am fine using the zero value (again, that's the majority of my personal use-cases for maps at least), having to use a statement just to discard the extra bool is annoying.
  
Right, so this brings me back to a nice solution to both our use cases, which would be to initialize the map as either having a default or not having a default.  You don't have to check for failure, I don't have to worry about forgetting to check for failure... right? :D
 
I know they are not here yet, but this is the kind of thing that generics were intended for. Once we have generics, you will be free to create a type safe map wrapper that gives you whatever behavior you want. IIRC, a richer set of containers is one of the main reasons people want generics in go.
Reply all
Reply to author
Forward
0 new messages