Nil could be the zero value for type variables

502 views
Skip to first unread message

Will Faught

unread,
May 30, 2022, 9:39:53 PM5/30/22
to golang-nuts
Hello, fellow Gophers!

Currently, if I understand correctly, there's no expression for the zero value for a type variable:

type Map[K comparable, V any] struct {
    ks []K
    vs []V
}

func (m Map[K, V]) Get(k K) V {
    for i, k2 := range m.ks {
        if k2 == k {
            return m.vs[i]
        }
    }
    return zeroValue // cannot currently express this
}

This is a trivial example, but I've seen real questions about what to do in these situations.

Currently, if I understand correctly, the only way to do this is to declare a variable, and return that:

var zeroValue V
return zeroValue

Why not allow nil to be used as the zero value for type variables, to fill this gap?

return nil // == V(nil)

At runtime, nil would be the zero value for the specific type argument.

Nil could actually be interpreted as the zero value for every type, even outside of generics. The word "nil" means zero, anyway. This would be handy in situations where you make a type a pointer just to avoid lots of typing. For example:

if condition1 {
    return ReallyLongStructName{}, fmt.Errorf(...)
}
if condition2 {
    return ReallyLongStructName{}, fmt.Errorf(...)
}

Instead, you could keep the non-pointer type, and then do:

if condition1 {
    return nil, fmt.Errorf(...)
}
if condition2 {
    return nil, fmt.Errorf(...)
}

It would also solidify the type abstraction concept of generic functions, where functions varying only in concrete types can be abstracted into one generic function with type variables. For example:

// Same implementations, different types

func (m MapIntInt) Get(k int) int {
    for i, k2 := range m.ks {
        if k2 == k {
            return m.vs[i]
        }
    }
    return nil // nil, not 0, but means the same thing for int
}

func (m MapStringString) Get(k string) string {
    for i, k2 := range m.ks {
        if k2 == k {
            return m.vs[i]
        }
    }
    return nil // nil, not "", but means the same thing for string
}

// Same implementation, abstracted types

func (m Map[K, V]) Get(k K) V {
    for i, k2 := range m.ks {
        if k2 == k {
            return m.vs[i]
        }
    }
    return nil // nil is 0 for int and "" for string
}

Similar to how generics required an inversion of the meaning of interfaces to be type sets, perhaps generics also requires an inversion of the meaning of zero and nil values, where every type has a nil (default) value, and for number-like types, that just so happens to be the number zero, but for other types, it could be whatever makes sense for them.

Anyway, I thought it was an interesting idea. Thanks for reading!

Bruno Albuquerque

unread,
May 30, 2022, 10:34:40 PM5/30/22
to Will Faught, golang-nuts
This should work on your example:

var zeroValue V

--
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/CAKbcuKjePVMPH1Fxv3tWAAm_msjAZKmtbJs2EMkQ7WGEgtAqyA%40mail.gmail.com.

Will Faught

unread,
May 31, 2022, 12:15:36 AM5/31/22
to Bruno Albuquerque, golang-nuts
Right, I discussed that:

Currently, if I understand correctly, the only way to do this is to declare a variable, and return that:
var zeroValue V
return zeroValue

Ian Lance Taylor

unread,
May 31, 2022, 9:19:43 PM5/31/22
to Will Faught, golang-nuts
On Mon, May 30, 2022 at 6:39 PM Will Faught <wi...@willfaught.com> wrote:
>
> Currently, if I understand correctly, there's no expression for the zero value for a type variable:

Thanks for the note. There is some discussion of this at
https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#the-zero-value
.

> Why not allow nil to be used as the zero value for type variables, to fill this gap?

We could do that. The main concern is that "nil" is already
overloaded, and people new to Go are frequently confused by it. See
the FAQ entry https://go.dev/doc/faq#nil_error . Adding more uses of
nil will increase the potential for confusion.

Ian

Will Faught

unread,
May 31, 2022, 11:45:29 PM5/31/22
to Ian Lance Taylor, golang-nuts
Thanks for pointing out that section. I guess I'd forgotten this issue was discussed there.

It looks like this exact approach (nil for all types) isn't listed there, but there's a similar one that adds nil to just type variables. Please add this one to your mental list! :)

Fair enough about the nil confusion, although perhaps the confusion stems more from the invisible assignability of a "raw"/unconverted nil to various types, rather than those types having a default/nil value. If Go went down this path, perhaps the confusion could be ameliorated by changing how the language spec introduces the idea of zero (now nil) values (by moving the zero value [now nil] specification closer to, or within, the type section, or something like that).

Thanks for your thoughts.

Brian Candler

unread,
Jun 1, 2022, 3:05:20 AM6/1/22
to golang-nuts
On Wednesday, 1 June 2022 at 02:19:43 UTC+1 Ian Lance Taylor wrote:
We could do that. The main concern is that "nil" is already
overloaded, and people new to Go are frequently confused by it. See
the FAQ entry https://go.dev/doc/faq#nil_error . Adding more uses of
nil will increase the potential for confusion.

I think this is an interesting proposal. If "nil" were to mean "the zero value of *any* type" then you can argue it removes a layer of confusion. Not only is it the zero value for pointers, interfaces, slices, maps and channels, but also for strings, integers, structs, and anything else that comes along.

Where confusion might arise is in the operations on nil.  It's already weird that nil slices and zero-length slices are distinguishable:

We'd then end up in the same position with strings, depending on how exactly the nil value of a string is defined:

var s2 string
if s2 == nil { ... }  // but this is different to "" ?

There would be an incentive for Go APIs to treat nil strings and empty strings differently (especially in SQL, JSON/YAML etc), and that would certainly be a bad thing IMO.

And you might get some other strange stuff, like being able to do arithmetic on nil.

a := 3
b := a + nil   // b := a + 0  ??
var c float64 = nil + nil   // ???

Axel Wagner

unread,
Jun 1, 2022, 3:49:50 AM6/1/22
to Brian Candler, golang-nuts
On Wed, Jun 1, 2022 at 9:05 AM Brian Candler <b.ca...@pobox.com> wrote:
On Wednesday, 1 June 2022 at 02:19:43 UTC+1 Ian Lance Taylor wrote:
We could do that. The main concern is that "nil" is already
overloaded, and people new to Go are frequently confused by it. See
the FAQ entry https://go.dev/doc/faq#nil_error . Adding more uses of
nil will increase the potential for confusion.

I think this is an interesting proposal. If "nil" were to mean "the zero value of *any* type" then you can argue it removes a layer of confusion. Not only is it the zero value for pointers, interfaces, slices, maps and channels, but also for strings, integers, structs, and anything else that comes along.

Where confusion might arise is in the operations on nil.  It's already weird that nil slices and zero-length slices are distinguishable:

We'd then end up in the same position with strings, depending on how exactly the nil value of a string is defined:

I don't think we *could* define it in that way. The zero value of a string must always be identical to the empty string, so as to not break existing code. That is, `nil` would become a way to write "", effectively.
 
There would be an incentive for Go APIs to treat nil strings and empty strings differently (especially in SQL, JSON/YAML etc), and that would certainly be a bad thing IMO.

And you might get some other strange stuff, like being able to do arithmetic on nil.

a := 3
b := a + nil   // b := a + 0  ??
var c float64 = nil + nil   // ???

Theoretically, that depends on how this was actually done. Currently, the pre-declared identifier `nil` is not actually a value, as such. The spec treats it as a special case for comparisons and assignability, for example.

I think we would have to continue making it a special case in many if not most aspects. It might be tempting to define `nil` as an untyped constant, but then we'd have to give it a default type and there does not seem to be a good option.

I think if we'd a) made the predeclared identifier `nil` assignable to any type and b) expanded the special case for comparisons to all types, we'd get the desired effect without allowing arithmetic expressions like these.

That being said, I also don't see a lot of harm in allowing them, if we make `nil` represent the zero value of any type. It is true that the code reads strangely, but we don't have to disallow all strange code.

Lastly, with all this in mind: I agree with Ian that overloading `nil` further is probably not good, even though we could. Most questions about it come in the form of "That function returns nil, but if I compare to nil it's false", or "I get a nil-pointer exception, but I checked for nil right before that" and those definitely wouldn't get any less confusing.


--
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.

Brian Candler

unread,
Jun 2, 2022, 5:03:04 AM6/2/22
to golang-nuts
On Wednesday, 1 June 2022 at 08:49:50 UTC+1 axel.wa...@googlemail.com wrote:
Where confusion might arise is in the operations on nil.  It's already weird that nil slices and zero-length slices are distinguishable:

We'd then end up in the same position with strings, depending on how exactly the nil value of a string is defined:

I don't think we *could* define it in that way. The zero value of a string must always be identical to the empty string, so as to not break existing code. That is, `nil` would become a way to write "", effectively.

Yes,  that's true - the zero value of string is defined to be "the empty string", regardless of what this means in terms of underlying pointer and len. And you can use foo == "" to test this.

I wonder then why slice wasn't defined the same way - i.e. the zero value of a slice could have been "empty slice". Today, empty slice and nil slice are distinguishable - even if the empty slice has cap 0.

I guess the problem is deciding whether it's cap(x) == 0 or len(x) == 0 which is the defining characteristic of the zero value.
 
I think if we'd a) made the predeclared identifier `nil` assignable to any type and b) expanded the special case for comparisons to all types, we'd get the desired effect without allowing arithmetic expressions like these.

That would work.  Hence:

    if v == nil { ... }   // v has zero value, regardless of its type

    v = nil   // reset to zero value of its type

    return nil   // zero value return

Lastly, with all this in mind: I agree with Ian that overloading `nil` further is probably not good, even though we could. Most questions about it come in the form of "That function returns nil, but if I compare to nil it's false", or "I get a nil-pointer exception, but I checked for nil right before that" and those definitely wouldn't get any less confusing.

I believe the fundamental thing we're talking about here is the fact that an interface value can be nil, or it can contain a typed value, which for certain types may also happen to be nil.  That issue isn't going away. The question is, does this proposal make it any worse?

The only thing you can do with interface values, apart from calling methods on them, is to check their nilness.  After doing Go for a while, I realised that

    if abc == nil { ... }

where "abc" is of an interface type, can only *possibly* be testing whether the interface contains a value or not.  It could not possibly be testing the value inside the interface for nilness, because we have no idea what type that is: e.g. it could be a struct, or a pointer to a struct, or a named type based on int64, and not all of those are comparable to nil.

Therefore, if all values became comparable to nil, then maybe people would have a stronger reason for expecting "abc == nil" to act on the value contained in the interface, rather than the interface itself.

There have been various proposals to solve that.  One is to have a different constant for nil interface values, but then

    if err != nilinterface { ... }

is fugly.  You might as well introduce new syntax for testing nilness of interfaces only:

    if has(err) { ... }

    if err.(any) { ... }

Or even make interfaces (only) usable directly in boolean contexts:

    if err { ... }

It would be a sort of half-way type assertion: this interface contains some value, but I don't care what it is.  It doesn't really solve the underlying issue, but since you've not done an explicit comparison with 'nil', maybe it's less surprising when you unwrap the value and find it contains nil.

Axel Wagner

unread,
Jun 2, 2022, 5:30:12 AM6/2/22
to golang-nuts
This is increasingly off-topic (I don't have anything to add to the on-topic stuff itself), but.

On Thu, Jun 2, 2022 at 11:03 AM Brian Candler <b.ca...@pobox.com> wrote:
I wonder then why slice wasn't defined the same way - i.e. the zero value of a slice could have been "empty slice". Today, empty slice and nil slice are distinguishable - even if the empty slice has cap 0.

I guess the problem is deciding whether it's cap(x) == 0 or len(x) == 0 which is the defining characteristic of the zero value.

I don't know why it was designed like that. I don't think this would have been a hard question, though.

The important question is "what does a == nil do?", as that's the only thing that's different between them.
So you could've just disallowed comparison to nil, requiring the programmer to be explicit. Just as with strings or most other types.
If you would have wanted comparison with nil to work, you should have definitely made a==nil equivalent to cap(a)==0, as a slice with cap(a)>0 has behavioral differences to the zero value - e.g. a[:1] does not panic if cap(a)>0.

In any case, it didn't end up that way. So by now the point is moot.

I think if we'd a) made the predeclared identifier `nil` assignable to any type and b) expanded the special case for comparisons to all types, we'd get the desired effect without allowing arithmetic expressions like these.

That would work.  Hence:

    if v == nil { ... }   // v has zero value, regardless of its type

    v = nil   // reset to zero value of its type

    return nil   // zero value return

Lastly, with all this in mind: I agree with Ian that overloading `nil` further is probably not good, even though we could. Most questions about it come in the form of "That function returns nil, but if I compare to nil it's false", or "I get a nil-pointer exception, but I checked for nil right before that" and those definitely wouldn't get any less confusing.

I believe the fundamental thing we're talking about here is the fact that an interface value can be nil, or it can contain a typed value, which for certain types may also happen to be nil.  That issue isn't going away. The question is, does this proposal make it any worse?

The only thing you can do with interface values, apart from calling methods on them, is to check their nilness.  After doing Go for a while, I realised that

    if abc == nil { ... }

where "abc" is of an interface type, can only *possibly* be testing whether the interface contains a value or not.  It could not possibly be testing the value inside the interface for nilness, because we have no idea what type that is: e.g. it could be a struct, or a pointer to a struct, or a named type based on int64, and not all of those are comparable to nil.

Therefore, if all values became comparable to nil, then maybe people would have a stronger reason for expecting "abc == nil" to act on the value contained in the interface, rather than the interface itself.

There have been various proposals to solve that.  One is to have a different constant for nil interface values, but then

    if err != nilinterface { ... }

is fugly.  You might as well introduce new syntax for testing nilness of interfaces only:

    if has(err) { ... }

    if err.(any) { ... }

Or even make interfaces (only) usable directly in boolean contexts:

    if err { ... }

It would be a sort of half-way type assertion: this interface contains some value, but I don't care what it is.  It doesn't really solve the underlying issue, but since you've not done an explicit comparison with 'nil', maybe it's less surprising when you unwrap the value and find it contains nil. 

--
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.

Nigel Tao

unread,
Jun 6, 2022, 12:44:37 AM6/6/22
to Will Faught, golang-nuts
On Tue, May 31, 2022 at 1:39 AM Will Faught <wi...@willfaught.com> wrote:
Why not allow nil to be used as the zero value for type variables, to fill this gap?

return nil // == V(nil)

Will Faught

unread,
Jun 11, 2022, 12:58:32 AM6/11/22
to Nigel Tao, golang-nuts
Thanks. I think these are more complicated and less consistent, since they require adding syntax for `_`, but I can see a similar motivation.
Reply all
Reply to author
Forward
0 new messages