5 ms·
A great innovation of Go is that it includes a built-in reference type with both inner nils and outer nils.
by anonymoushn 5y ago
A great innovation of Go is that it includes a built-in reference type with both inner nils and outer nils.
- kubb 5y agoIt is a bit like void *outer = 0; // outer can be nil int val = 5; int *inner = &val; // inner can also be nil outer = (void*)(&inner) // can have an inner value, inner nil or outer nil
- masklinn 5y agoIt's not really like that, this is two nested but different values which can be nil (not exactly independently but close). typed nils are a single value which is both nil and non-nil at the same time.
- throwaway894345 5y agoYou're mistaken. An interface is simply a reference type--like other reference types, it may refer to a type which is itself a reference type. The outer reference may be not-nil while the inner reference may be nil, which confuses people who don't understand references into believing that there is a single value which is both nil and not-nil at once. It's no different than `var x **int` (a variable called "x" whose type is a pointer to a pointer to an int). If you want to safely access the underlying integer data, it's not sufficient to check that the outermost reference is not nil--you must also verify that the inner reference is not nil. We run into this in every language with nullable reference types, including C, Java, Python, JavaScript, etc. EDIT: `*int` -> `**int`. TIL HN converts two asterisks into one.
- masklinn 5y ago> An interface is simply a reference type--like other reference types, it may refer to a type which is itself a reference type. The outer reference may be not-nil while the inner reference may be nil, which confuses people who don't understand references into believing that there is a single value which is both nil and not-nil at once. None of that is correct. The value-part of the fat pointer is the exact same in both situations: nil. It is a single value which is both nil and non-nil. > It's no different than `var x **int` I assume you meant that rather than what you actually wrote. It is in fact quite different. > If you want to safely access the underlying integer data, it's not sufficient to check that the outermost reference is not nil--you must also verify that the inner reference is not nil. Something a typed nil does not let you do, unless you know (or reflect) the underlying concrete type in order to downcast the interface back to a regular pointer. > We run into this in every language with nullable reference types, including C, Java, Python, JavaScript, etc. We really don't. In "every language" you have to actually create a double indirection for something within the same heavenly realm (but is not at all the same thing) to occur.
- throwaway894345 5y ago> I assume you meant that rather than what you actually wrote. Yes, today I learned HN replaces double-asterisks with single asterisks. Edited my original post. > None of that is correct. The value-part of the fat pointer is the exact same in both situations: nil. It is a single value which is both nil and non-nil. I repeat, an interface isn't "both nil and non-nil", but rather the nullity of the interface and the nullity of the underlying data are distinct propositions. For example, there is nothing analogous to "both nil and non-nil" when the runtime type is a value type, such as an integer. An interface behaves just like a reference. When the underlying data is also a reference, then it behaves like a reference to a reference. This means you can have a nil interface or a non-nil interface which references a nil pointer. Under the covers, Go implements this behavior by representing interfaces as a tuple where the first field contains a pointer to the runtime type information and the second field contains a pointer to the data. Go encodes a nil interface as (nil, nil) while it encodes an interface backed by a nil int pointer as `(<pointer to `int` type>, nil)`. > We really don't. In "every language" you have to actually create a double indirection for something within the same heavenly realm (but is not at all the same thing) to occur. Go interfaces to pointer data are functionally "double indirection". The outer pointer can be either nil or a pointer to the data which itself can be nil or valid. > Something a typed nil does not let you do, unless you know (or reflect) the underlying concrete type in order to downcast the interface back to a regular pointer. Well, this would be unsafe as the runtime type of the underlying data isn't guaranteed to be a pointer. So by default Go requires runtime type checks in this case, but you can always opt out of these runtime type checks via `unsafe` but this is usually inadvisable.
- Skunkleton 5y agoAs a non-serious user of Go, this was something I found to be a negative. What am I missing?
- masklinn 5y agoThat an innovation is not necessarily positive. For instance INTERCAL innovated COMEFROM. Also that GP was probably sarcastic when they nominated typed nils.
- Skunkleton 5y agoOh good. I’m glad that was just a wooosh.
- azth 5y agoA negative innovation, sure :P
- throwaway894345 5y agoGo has no such feature. Go's interfaces are a reference type which point to the runtime implementation. If that implementation is itself a pointer type, then that pointer may also be nil. Any language that has nullable reference types (including languages in which everything is a pointer like Java or Python) will have this problem. EDIT: would really like to know why I'm being downvoted. If you disagree with my comment, then best of luck to you in rebutting it. :)
- masklinn 5y ago> Go has no such feature. [...] Any language that has nullable reference types (including languages in which everything is a pointer like Java or Python) will have this problem. var i *int = nil var x interface{} = i if x != nil { fmt.Printf("%v was not nil", x) } <nil> was not nil By all means, do demonstrate how Java or Python have this problem.
- throwaway894345 5y agoSure: class Ptr: def __init__(self, x): self.x = x x = Ptr(None) if x != None: # Go's `%v` print directive unpacks interfaces. # We'll use `x.x` to emulate the same here print(f"{x.x} was not None")
- kinjba11 5y agoI don't really see a problem with that code - there is no confusion, `Ptr(None)` is clearly not `None`. Whereas var i *int = nil var x interface{} = i if x != nil { I assigned `nil` to `x` and yet `x` is not nil. Implicit conversions aren't unusual in programming but in Go they stand out a bit. The closest analog I could find was abusing implicit conversion operator in C#. But even that's pretty explicit and you can tell something odd is happening. Thing1 i = null; Thing2 x = i; if (x != null) { System.Console.WriteLine("x is not null"); } class Thing1 { public static implicit operator Thing2(Thing1 c) => new(); } class Thing2 {}
- Ericson2314 5y agoWell played :)
- jerf 5y agoThey are not the same nil: http://www.jerf.org/iri/post/2957 http://www.jerf.org/iri/post/2957
- masklinn 5y agoThat they are technically different nils if you split hair enough really has no bearing on the user hitting those issues, with no real way to work around them, and no warning from the tooling. And your explanation does not really hold in actuality: type DoesAThing interface { Thing() } type ThingDoer struct { } func (td ThingDoer) Thing() { } func WillPanic() { var thingDoer *ThingDoer var someThing DoesAThing = thingDoer someThing.Thing() } There is no situation in which calling Thing() on a ThingDoer will break, but Go's author have decided that implementing an interface on a value also implements it on the pointer, I never said to do that and carefully did not do that. This means when you assert that > the nil is an invalid implementation, not because it is an invalid Go value, but because it is an invalid implementation of the interface. The design of Go forces that lie on you, you have no way to opt out of it. If you implement interface methods on a value receiver, that lie will be forced onto you with no recourse. If you implement interface methods on a pointer receiver, then that lie will still be forced onto you but give you the option of... defensively ignoring and maintaining garbage state (à la ObjC I guess).
- jerf 5y ago"That they are technically different nils if you split hair enough really has no bearing on the user hitting those issues, with no real way to work around them, and no warning from the tooling." They have different types. This is not splitting hairs. "The design of Go forces that lie on you, you have no way to opt out of it." No, it doesn't. I've been programming in Go for about 8 years now, and I do not face this problem, because I do not lie in my code this way. My own experience is constructive proof of my point. That it, it falsifies your point, if this was "forced" on me I'd be encountering this problem. It is not a problem you have to have in Go. I can and have rewritten any code that has this problem when people post their code where they are "forced" to have this problem, and this is the cause every time. It is a result of bad programming decisions. Do not create objects that can not fulfill their contract, and you won't have problems with them propagating through your code. (I mean, I'm really shocked at how much this angers people. Who is really advocating for "Create whatever garbage you want where ever and get angry at the downstream code when it can't pick up the pieces?" I mean, sure, it's a common style de facto, but who is advocating for this?) Go is far from the worst case of this, the dynamic typing world affords code at scale that does this so much that it simply becomes a fact of life. It's the biggest reason I've come to loathe writing any code at scale in dynamic languages.