8 ms·
Nil in Go is typed in theory and sort of untyped in practice
- temac 6y agoI'm not sure why it would be significantly better to have reliably "typed nil" over "untyped nil". The concept has no real definition to begin with: what matters about sound typing are the static guarantees a type brings, or by association even if this not really the same concept, the "dynamic guarantees" about the behavior of objets of some "dynamic type" (that is the variable used as a handle to the objet had no guarenteed static type, but in most cases the object themselves have a type). Acting on a "type" of nil values seems to make very little sense. Maybe there are obscur constructs where this would be handy, but at the first glance I would consider it probably indicative of a dirty hack, and recommend looking for alternate solutions.
- stcredzero 6y agoIn Smalltalk, nil was a constant that contained the only instance of the class UndefinedObject. What some people did, was to put methods on UndefinedObject to implement a "NullObject" pattern for their domain objects. Since everything is default initialized to nil, everything would just work. The big downside of this, were beginners putting methods on UndefinedObject that they should not have. There's a thin line of conceptual understanding between a bit-bucket that hides errors from you, and a useful NullObject. Some people would call this a "dirty hack" for that reason.
- curryst 6y agoIt's probably not useful in Go's type system. It would be useful if there were union types, though. I sometimes end up using pointers for nullable fields because the zero values are significant in whatever domain I'm working in. I either have to make it a pointer, or I have to implement a new special zero value, or I have to make a struct that can keep track of whether a value has been set or not. It'd be nice to be able to do type MyStruct struct { ID int || nil } And have the compiler understand that the ID has to be either an int or nil, and to give me errors if I try to use it as an int without making sure it isn't nil.
- kazinator 6y agoI'm afraid this rings slightly hollow for me. We can likewise claim about C that "0 in C is typed in theory and sort of untyped in practice" because constant expressions of integer type that evaluate to 0 can serve as null pointers of any type: char *p = 0; // no cast required if (p == 0) ... // no cast required time_t t = time(0); // fine char *q = 1; // error; requires (char *) 1. We can't think of this 0 as an object; it's just a source code token that provides a null pointer of the right type, which is statically obvious from the use. The only time it's a problem is in unsafe interfaces: int old_style_string_fun(); int variadic_fun(char *first, ...); old_style_string_fun(0); variadic_fun("first", 0); Here, the wrong type is inferred; the 0 just behaves like the ordinary constant of type int. A null pointer to char was intended, which requires (char *) 0 must be used. None of this challenges the fact that we can have a variable of type int, whose value is 0.
- JeremyBanks 6y agoC is an ~untyped language so this comparison isn't very helpful.
- kazinator 6y agoThough a popular meme, it is false. C has an extensive static type system, requiring programs to be checked and diagnosed. This is quite helpful in catching bugs and refactoring and all that. That type system doesn't encode everything that some people would like, such as preventing a program from retaining and using a pointer to a datum that no longer exists. It allows implicit conversions that some people would like to be explicit (e.g. integer to floating-point and vice versa), but in those situations, the static type of every operand, and of the result, is known and the compiler chooses the conversion accordingly.
- kazinator 6y agoAddendum: C is untyped in linkage: the combination of multiple translated units into a single program. All of the type checking in C translation happens within a single translation unit. The language standard makes no requirements regarding the retention of type information in the translated code. An identifier declared as "double foo" in one translation unit at the file scope, as an external name, could be misused in another translation unit via "extern int foo". Similar reasoning applies to function arguments and structure definitions. We can easily write a program in which two translation units have a completely different idea about what is "struct foo", such that one creates a struct foo and passes a pointer to that into the other. This could happen by accident, at least in the development environment even if the declaration is written in one place (a header file) and everything else includes it. How? Quite simply, the build system could have broken dependencies, and fail to recompile the second translation unit when a declaration has changed. In practice, adherence to program organization conventions, additional diagnostics available in compilers, and reliable compiler-driven dependency generation fill this gap, though imperfectly. Plus related CI, QA and delivery practices, like never giving a build to testers or customers that was an incremental build from a developer's system; every officially tested build must be a clean rebuild. There are related issues in the maintenance of libraries, which give rise to situations that dependent code simply will not and cannot be recompiled, so binary compatibility must be maintained.
- neild 6y agoThe predeclared identifier nil is a constant, like 0 or "". Like 0 or "", nil is untyped. Go has both typed and untyped constants. 0 also is untyped, which is why you can write x==0 regardless of whether x is an int32 or an int64. Unlike 0 or "", nil does not have a default type. The default type of a constant is the type that it is implicitly converted to when a type is required. For example, "x := 0" declares a new value x with the value 0, but 0 is untyped. Untyped integer constants have a default type of int, so x is given the type int. Since nil does not have a default type, it's an error to write "x := nil". You can declare a typed constant with the value 0 or nil. int32(0) is a constant with type int32 and the value 0. (* int32)(nil) is a constant with the type pointer-to-int32 and the value nil. Variables of an interface type can hold any value that satisfies the interface. For example, a value of type io.Reader can hold any value that has a Read method with the appropriate signature. The common confusion about nil occurs because you can compare an interface value to a non-interface value. var b *bytes.Buffer // b is a variable with type *bytes.Buffer var r io.Reader // r is a variable with type io.Reader r = b // r now holds a value with type *bytes.Buffer and value nil b == nil // true: b is nil r == b // true: r and b are equal r == (io.Reader)(nil) // false: r is not a nil io.Reader r == (*bytes.Buffer)(nil) // true: r contains a nil *bytes.Buffer r == nil // false: r is not a nil io.Reader The confusing part is that last line. How can r not be nil, when it contains a nil value? And the reason is hopefully apparent from the previous two lines: It depends on the type of "nil". Whether you find this a wart in the language or not, this is not a case of nil being either typed in theory or untyped in practice. The predeclared identifier "nil" is an untyped constant.
- mh- 6y agoexceptionally clear explanation, thank you.
- morelisp 6y ago> Like 0 or "", nil is untyped. Go has both typed and untyped constants... Unlike 0 or "", nil does not have a default type. So it feels like you're trying to draw some distinction here that is like, "look, nil isn't really that special - there's groups A and B, and the core confusion is people think nil is in group A while the truth is in it's group B." And it's true, Go has both typed and untyped constants. But as far as I know, nil is the only untyped constant without default type. So "group B" is still just nil, and nil remains a sui generis source of confusion in Go.
- miga 6y agoIt is very confusing that equality in JavaScript is not transitive: https://imgur.com/5pFXFbR https://imgur.com/5pFXFbR. Apparently equality in Go is no longer transitive either...
- jerf 6y agoI think I'd need to see an example of that to be sure. What's talked about here doesn't really lead to that, because in these examples, values are getting passed through a function call boundary. There's a lot of rules in Go for what == does, and maybe there's something non-transitive in there, but I'd definitely need to see someone prove it on https://play.golang.org/ https://play.golang.org/ to be sure. Offhand I can't think of how to make one.
- morelisp 6y agoThere is a nontransitive example heading this very comment section: var b *bytes.Buffer var r io.Reader = b fmt.Printf("r == nil: %v; b == nil: %v; r == b: %v\n", r == nil, b == nil, r == b) r == nil: false; b == nil: true; r == b: true
- jerf 6y agoThank you, I missed that.
- cratermoon 6y agoIt's not confusing if you understand that r is pointer that points to something and b is that pointer, which is nil. So of course r (the variable containing a pointer) isn't nil, b (the pointer) is nil, and r is the pointer b.
- Dylan16807 6y agoPointers don't normally compare equal to the thing they're pointing to. If it's acting as a proxy it's confusing the specific way that some things get proxied and other things don't.
- macintux 6y agoMuch discussion about nil in Go just yesterday: https://news.ycombinator.com/item?id=26635529 https://news.ycombinator.com/item?id=26635529
- jerf 6y agoFor some context, I've been programming in Go for about six years now, and I've literally never even thought about this. In general, you really haven't got much reason to be worried about something's "interface type", because if you're really worried about something's "type" (which can happen) you are almost certainly worried about it's "concrete type". The only interesting thing you can ask about the interface type directly would be "Does the value inside this interface implement this interface?", and the answer is statically yes, the compiler assured that, and the interface type is statically assured at runtime, so it's not a very interesting question. Even if fmt added a way to "see" the interface type passed in, I can't imagine what it would be useful for, because it could only ever be: var x SomeInterfaceType // some number of lines of code fmt.Printf("%I\n", x) which would statically print "SomeInterfaceType". So, I mean, it's not like anything said in that blog post is false necessarily, but if you're collecting reasons to hate on Go's nil handling, there's no real justification in adding this to your list. I'd also add that I answer newbie questions a lot on Reddit, and nothing even remotely like this has ever come up there, either.
- autarch 6y agoI agree that you won't think about this too often. However, if you're trying to use the reflect package to get type information about variables, you may end up having to think about it (and like me, you may end up feeling very confused!).
- deleted 6y ago[deleted]
- eptcyka 6y agoI programmed in Go for three years, and got bitten by this at least twice. Being able to determine if a value that implements an interface is nil or not would really be nice.
- waheoo 6y agoHow does nil implement an interface?
- ivanech 6y agoThis reminds me of Rich Hickey's "Maybe Not" talk
- deleted 6y ago[deleted]
- tapirl 6y ago'nil' in Go is just an identifier to represent the zero values of some kinds of type. A bare nil is always untyped and it is the only untyped value which has not a default type. Zero values behave the same as non-zero values in many aspects. More read: nils in Go https://go101.org/article/nil.html https://go101.org/article/nil.html The facts that several kinds of type share the same zero value literal representation really confuse many new gophers, but this is hardly a problem when you get familiar with the design. There are proposals to remove the new-gopher confusion. * https://github.com/golang/go/issues/22729 https://github.com/golang/go/issues/22729 use different zero identifiers for different kinds of type * I remembered that there is a propsoal which propose using `null` for non-interface zero values but still using `nil` for interfaces. I couldn't find this issue now.