*From Jan Mercl: *

> To represent all those values and the new possibility
> of the value being nil, we need 257 distinct values. But that does not
> fit 8 in bits anymore. However, changing the size and/or the bit
> representation of such a variable is observable, making it not
> backwards-compatible.


I'm not proposing that *any* value be made nillable, I'm proposing the
explicit syntax

var x uint8 | nil

that would create a nillable uint8. A variable of type `byte` would still
only take up one byte; a variable of type `byte | nil` would be larger.
Existing code, which obviously doesn't use the `| nil` syntax because it
doesn't exist yet, would be completely unaffected by this change.

*From Brian Candler:*

> I also think that you need to find multiple compelling use cases (of which
> that example is not one), before fundamentally adding complexity to the
> language. Remember that "nil" is already overloaded in Go, so adding
> another (similar but different) meaning increases complexity for the reader.


Back when I worked at google, this was the number one cause (at least in
code that I worked with) of runtime errors in go (mostly in code using
protobuf v2 or ygot). I've also seen a lot of random github discussions
that boiled down to "how do we differentiate between 'x is unset' and 'x is
explicitly 0'; if I have some time next weekend I'll see if I can make a
big list of those to support the motivation for this.

*From Jeremy French:*

> I understand the problem you are trying to solve, and it's valid, I
> think.  But this solutions "feels" bad and very un-Go-like.  Usable zero
> values is a feature in Go. Whether you agree with that or not, it's a
> selling feature of the language and clearly something that was/is important
> to the Go Authors - as is explicit vs implicit logic.


This would make zero values *more* usable - it provides a way to ensure
that the zero value of a struct is a valid and consistent empty value of
that type, without requiring that users remember to set extra bools or do
nil checks. `Certificate` is a good example of this - you would expect a
"zero" certificate to not have a path length specified, but since 0 is a
possible value for MaxPathLen in this context, the type needs the extra
bool because otherwise `Certificate{}` would give you a certificate that
asserts that its subject is a CA and that this certificate cannot be
followed by any other intermediate certificates.

And, to be clear, the developers of that library could have left the bool
out - they already have the logic that -1 indicates an unset length, so
they could've just insisted that anyone who created a Certificate must
remember to explicitly set its MaxPathLen to -1. They added this extra
boolean solely because they wanted the zero value `Certificate{}` to
actually be an empty certificate, and the fact that they added a bunch of
extra logic (and new ways to make a logically inconsistent certificate) in
order to this is pretty strong evidence that this would be a useful thing
for the language to support.

It would not be an extremely common thing to do: for most types, the zero
value really is usable! But in cases where the zero value of a field has an
actual important meaning, it would be really great if there were a standard
way to make zero values of structs still be useful.

 So having a zero value that implicitly means "don't use this" a a feature
> that is built into the language seems like it could lead to confusion or at
> least a very muddled message about the nature and purpose of Go.


As far as I know, nobody has this issue with pointers, which very
definitely have a zero value that means "don't use this"; I don't see how
using 'nil' this way would confuse anyone about the nature of Go.

Indeed, the most common way I've seen to solve this problem is just to use
pointers, and live with the fact that if a junior dev forgets a nil check
then your code will compile just fine but panic at runtime; this seems
antithetical to the nature of Go.

I think maybe where your opinion diverges from the "hive mind" here is that
> you want the compiler to enforce this notion.  You can already achieve your
> basic objective with your "Additional Bool" example, and the confusion you
> complain about there can be easily mitigated with a better named variable
> "IsSet" rather than "IsZero" like Brian Candler suggested.


This is a minor point, but in the example I gave, IsZero is only checked if
the value is zero, so that users can write `cert.MaxPathLen = 5` without
remembering to set the bool except in the less common case where you set it
to 0. It obviously wouldn't be hard to have an IsSet, but both cases add
operations or checks that users must remember to do to maintain
consistency, with no compile-time mechanism to validate that they actually
are doing this, and make it possible to construct inconsistent data (e.g.
setting Value to something non-zero but also setting IsSet to false).


> But you still wouldn't have compile-time enforcement.  And that I think is
> where Go is going to disappoint you.  Go is a strongly typed language, at
> least on the scale of QBasic to Pascal, but the compiler does allow you to
> do a lot of things that you shouldn't, in the interest of letting you do
> things that might be a little crazy but that still work.  It seems to me
> that Go likes to leave "should" rules to the linter rather than the
> compiler, so maybe you could use a custom linter to achieve what you're
> looking for?  It's not a perfect solution, but it feels like it gets you
> 90% there.


Being able to tell the compiler "tell me if this code uses a value that
isn't set" is a lot more important than "tell me if this code declares a
variable but doesn't use it", which as far as I know is the established bar
for whether a linter-type check should be enforced by the compiler or not.

I'm also not sure how one would build a custom linter for this. I guess you
could just use pointers and have your linter yell anytime you dereference a
pointer without first nil-checking it, and just add a lot of linter-ignore
comments in places where these checks aren't needed? That wouldn't help
with the issues around assignment or mutability, though - that would
require a preprocessor or something, which would be dramatically more
un-go-like. Or you could go with a Maybe[T] and make a linter that checks
that .Value is never accessed unless .IsSet is checked, and .IsSet is set
any time .Value is? That sounds like it would be pretty error-prone. Is
there a better way to achieve that?

Thanks,
Dan


On Tue, Mar 19, 2024 at 12:22 PM Jeremy French <ibi...@gmail.com> wrote:

> I understand the problem you are trying to solve, and it's valid, I
> think.  But this solutions "feels" bad and very un-Go-like.  Usable zero
> values is a feature in Go. Whether you agree with that or not, it's a
> selling feature of the language and clearly something that was/is important
> to the Go Authors - as is explicit vs implicit logic.  So having a zero
> value that implicitly means "don't use this" a a feature that is built into
> the language seems like it could lead to confusion or at least a very
> muddled message about the nature and purpose of Go.
>
> I think maybe where your opinion diverges from the "hive mind" here is
> that you want the compiler to enforce this notion.  You can already achieve
> your basic objective with your "Additional Bool" example, and the confusion
> you complain about there can be easily mitigated with a better named
> variable "IsSet" rather than "IsZero" like Brian Candler suggested.  But
> you still wouldn't have compile-time enforcement.  And that I think is
> where Go is going to disappoint you.  Go is a strongly typed language, at
> least on the scale of QBasic to Pascal, but the compiler does allow you to
> do a lot of things that you shouldn't, in the interest of letting you do
> things that might be a little crazy but that still work.  It seems to me
> that Go likes to leave "should" rules to the linter rather than the
> compiler, so maybe you could use a custom linter to achieve what you're
> looking for?  It's not a perfect solution, but it feels like it gets you
> 90% there.
>
> On Monday, March 18, 2024 at 4:42:32 AM UTC-4 Brian Candler wrote:
>
>> I like Go because it's relatively simple and low-level, like C, except
>> with things like garbage collection and channels integrated.  If Go were to
>> have "| nil" types then the internal representation of such variables would
>> have to be something like this:
>>
>> type Maybe[T any] struct {
>> Value T
>> IsSet bool
>> }
>>
>> In which case, why not just use that form explicitly? The code you write
>> is then clear and obvious. Taking one of your examples:
>>
>> func foo3(x Maybe[int]) {
>>   if x.IsSet {
>>     fmt.Println("x squared is", x.Value*x.Value)
>>
>>   } else {
>>     fmt.Println("No value for x")
>>   }
>> }
>>
>> // https://go.dev/play/p/ci10fhU1zqL
>>
>> I also think that you need to find multiple compelling use cases (of
>> which that example is not one), before fundamentally adding complexity to
>> the language. Remember that "nil" is already overloaded in Go, so adding
>> another (similar but different) meaning increases complexity for the reader.
>>
>> Full union types are a different thing again, and I can see the
>> attraction of those - except at that point, the language is no longer Go.
>> Having "int32 | int64" as a union type, but also as a generic type
>> constraint, would also be somewhat mind-boggling. How would you write a
>> type constraint that allows a union type?
>>
>> On Monday 18 March 2024 at 07:01:25 UTC Jan Mercl wrote:
>>
>>> On Mon, Mar 18, 2024 at 4:41 AM Daniel Lepage <dple...@gmail.com>
>>> wrote:
>>>
>>> > This change would be entirely backward-compatible ...
>>>
>>> Let's consider, for example, the type uint8, aka byte. A variable of
>>> type byte is specified* to occupy 8 bits of memory and has 256
>>> possible values. To represent all those values and the new possibility
>>> of the value being nil, we need 257 distinct values. But that does not
>>> fit 8 in bits anymore. However, changing the size and/or the bit
>>> representation of such a variable is observable, making it not
>>> backwards-compatible.
>>>
>>> ----
>>> *: From https://go.dev/ref/spec#Numeric_types:
>>>
>>> """"
>>> The value of an n-bit integer is n bits wide and represented using
>>> two's complement arithmetic.
>>> """"
>>>
>> --
> 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+unsubscr...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/golang-nuts/e45d298f-154d-49c1-9316-73df4f250543n%40googlegroups.com
> <https://groups.google.com/d/msgid/golang-nuts/e45d298f-154d-49c1-9316-73df4f250543n%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>

-- 
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+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/CANtoCQ9JP2VZLdQtrhy9FD2hZ%3Dr_iRpfXa8nY%3DUUB6hZ6LL9KQ%40mail.gmail.com.

Reply via email to