[HN Gopher] Nil in Go is typed in theory and sort of untyped in ... ___________________________________________________________________ Nil in Go is typed in theory and sort of untyped in practice Author : pcr910303 Score : 90 points Date : 2021-03-30 12:14 UTC (1 days ago) (HTM) web link (utcc.utoronto.ca) (TXT) w3m dump (utcc.utoronto.ca) | ivanech wrote: | This reminds me of Rich Hickey's "Maybe Not" talk | temac wrote: | I'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 wrote: | In 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. | kazinator wrote: | I'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 wrote: | C is an ~untyped language so this comparison isn't very | helpful. | jerf wrote: | For 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 wrote: | I 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!). | eptcyka wrote: | I 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 wrote: | How does nil implement an interface? | eptcyka wrote: | A pointer to a type that implements an interface can be | nil, but a function that takes said pointer as an interface | type will not be able to distinguish between a pointer that | points to an allocated object and a nil one. | [deleted] | neild wrote: | The 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. | stcredzero wrote: | _0 also is untyped, which is why you can write x==0 regardless | of whether x is an int32 or an int64..._ | | _Since nil does not have a default type, it 's an error to | write "x := nil"._ | | But not an error to write "x := 0"? | masklinn wrote: | No, because as they note constants can have a default | type[0]. For integer constants, that's `int`. Meaning if a | literal integer constant is not otherwise typed, it will | default to `int`. | | [0] https://blog.golang.org/constants#TOC_5. | saghm wrote: | The next paragraph they wrote explains this: "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." | dhagz wrote: | Because 0 has a "default type" of int. So when the compiler | goes to determine the type of x, it sees its set to 0, which | the compiler assumes to be an int. | | I've found thinking about nil too much in Go hurts or creates | weird loops in my brain, so I just tend to worry about it | when I'm dealing with interfaces or pointers. Everything else | I'm looking at zero-values. | stcredzero wrote: | I'd much prefer it if in golang, the type of nil was some | "nilType" for which != and == is defined for all other | types. Or, define some other operation, like isNil() | | Then determining if something is nil could be done without | thinking about it at all. nil would be nil would be nil. | (Also, it would be illegal to put methods on nilType, of | course.) | | (Yes, I make my living writing go, and this would make my | life easier. And no, in that case, don't allow x := nil.) | mh- wrote: | exceptionally clear explanation, thank you. | esrauch wrote: | r==b and b==nil but r!=nil seems like the kind of thing that | people give JS a lot of crap over. | mirekrusin wrote: | Can you please give more concrete example that is valid js | but is absurd logically? | camjw wrote: | I think the classic example is [] == ![] evaluates to true. | It's not absurd logically, just the operators act | weirdly/non-standardly. | mirekrusin wrote: | Yeah, that one is funny > [] == [] | false > [] == ![] true | mirekrusin wrote: | However typescript says: This condition | will always return 'false' since the types 'never[]' and | 'boolean' have no overlap. | | ...for `[] == ![]`. Which is ironic because it does | return true, not false. However at least it flags an | error! | | I wonder how much of weirdness is wiped out by | linters/ts/etc. | qsort wrote: | Honestly that's more of a weirness of the '==' operator, | which is kind of deprecated in modern JS. On the other | hand I fully agree that non-strict definitions of | equality are bound to generate weird cases around nullish | values. | yoneda wrote: | Huh, so equality isn't transitive in Go. Yikes. | Spivak wrote: | Eh, languages end up with this all the time with type | conversions. | | In JS: false == undefined => false | false == null => false undefined == null => true | masklinn wrote: | First, your comment makes no sense: a = 1 | b = 2 c = 1 a == b => false b == c | => false a == c => true | | Is that surprising? Equality is transitive, inequality is | not. | | Second, if you have to reach for old javascript "features" | (or got forbid PHP) to defend a language you're in a bad, | bad place. | | Third, the broken equality is not something that's usually | praised about javascript. In fact the first rule of | javascript equality is that there is only one situation in | which `==` is the correct operator: checking that something | is null (because that avoids having to differentiate | between null and undefined, papering over one javascript | wart using another). In every other situation you want | "strict" (===) equality. | virtue3 wrote: | I managed to get my team to use `lodash.isNil` because we | had gotten bitten by this crap too many times. And | `lodash.isEmpty` for strings/arrays/etc. | mirekrusin wrote: | What is absurd about this statement from logical point of | view? A != B A != C B == C | | ...looks like valid logical statement, ie. A = 1, B = 2, C | = 2: 1 != 2 1 != 2 2 == 2 | dragonwriter wrote: | That's not an example of nontransitivity. You've only shown | that some nontruthy values are unequal by == and some are | equal by ==. Nontransitivity would be a == b, b == c, c != | a. But you have the much more expected b != a, b != c, c == | a. | | JS loose equality is intransitive, though; the standard | example being: '0' == 0; // true 0 == | ''; // true '0' == ''; // false | siebenmann wrote: | The Go specification is careful to not call 'nil' a constant, | and in fact at one point specifically says that it isn't | ("Conversions", in the section on converting constants into | typed constants, which actually uses '(*int)(nil)' as an | example of something that is not a typed constant). Also, | although it wasn't clear in the entry, the original article | that it was a reaction to talked specifically about 'nil | variables' (ie, variables with the value of nil). | | (Even the concept of 'the value of nil' is tricky in Go; I | believe the specification only talks about things being | comparable to nil or allowing nil to be assigned to them. The | specification really goes to a lot of work to not treat nil as | a value, exactly. I suspect that the Go spec authors really did | not want a rerun of the C idea that the NULL pointer is '0' and | has an all-zero value and so on.) | | (I'm the author of the linked-to entry.) | morelisp wrote: | > 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. | 13415 wrote: | Yes, but giving nil a default type would be way more | confusing. | morelisp wrote: | Go already kind of does though, when you use it in a type | switch nil has type nil. For binding purposes, | `interface{}` also seems reasonable. (Or only half-joking, | if I wanted maximum pragmatism for minimal clarity, nil's | default type should be error.) | | It seems the root problem is that interfaces and concrete | values occupy the same semantic positions. But that's way | too hard a problem to fix; if I had a magic wand to update | all existing code I'd probably just have `== nil` on an | interface check for both kinds and try to rescue | transitivity. | brundolf wrote: | At the risk of inviting a flame-war, it's distressing to | me that a language this recent, with such a purported | focus on simplicity, has design-problems this fundamental | 13415 wrote: | Yes, that sounds like a flame war invitation. Issues with | nil plays no role in practice. | jerf wrote: | I have a hard time classifying this as a "design problem" | when it has zero practical implications. I've never | encountered a problem that can be traced back to this, | nor ever helped anyone else with a problem that can be | tracked back to this. If it is a "design problem" it is | of the weakest possible class. I can't imagine what would | "fix" it that wouldn't be a larger design problem of its | own. | | The whole "confused about nil interface vs. interface | containing nil pointer of some type", while I would | contend has a root cause of improperly importing the idea | from C++ that nil is automatically invalid when in fact | it is perfectly valid in Go to call methods on nil and | thus confusing "nil" with "invalid", is at least a cause | of real bugs and real misunderstandings. | morelisp wrote: | Sorry, you won't get much agreement from me on this. | There's only two kinds of languages, the kind with | fundamental design problems and the kind people don't | use. | | If you are looking for the perfect language may I suggest | Forth on a Z80? Otherwise there's going to be a pile of | shit hiding somewhere. | brundolf wrote: | I disagree. Issues like this on really core language | primitives usually crop up when either: | | 1) The language lacked hindsight in its domain because | there wasn't a ton of prior art (C) | | 2) The language didn't have enough foresight put into it | (JavaScript) | | 3) The language's modern usage has drifted significantly | far from its original usage (Java) | | Python is an example of a language people use that | doesn't suffer from #1 (thanks to Perl and friends), | doesn't really suffer from #2 (at least, Python 3) and | doesn't (yet) suffer too badly from #3. It has plenty of | flaws and limitations, but not in its foundation. The | core language primitives are rock-solid. | | #1 doesn't apply to Go, and it's not old enough for #3 to | apply. So that seems to only leave #2. | | (I should add that this doesn't make a language | completely invalid or useless. I use (and even like!) JS | despite its flaws; in fact I prefer it to Python overall, | mostly due to the ecosystem. It's just disappointing to | see issues of type #2, because they seem so avoidable.) | 13415 wrote: | It's pointless discussing this. There is no perfect | language and every programming language I've ever seen | has design problems at one level or another. I know | enough of CL, Pascal, Modula, Ada, Rust, Nim, etc., to | know their weaknesses, too. | | Your C example is also not convincing. Lisp and Algol | were already strong contenders at the time. People chose | C because they needed a fast and close to metal language | to create their own operating system for mainframes. It | was a hack just like your category 2. Still, it's a great | language. | brundolf wrote: | > There is no perfect language | | > It's pointless discussing this | | I don't see how the first implies the second. This is a | programming forum; it's good to discuss and debate the | specifics of these things. | | C may not have been the best example of the category, but | I think the category stands. Rust for example, much as I | love it, is exploring uncharted territory. I wouldn't be | surprised if 10 years from now we have ownership-based | languages that are much easier to use, simply because | lessons will have been learned by that point. Arguably | Java's OOP fundamentals are not as good as C#'s because | Java had to explore that territory first (setting aside | technicalities about what the term "OOP" really means). | Etc. | 13415 wrote: | The problem is that decades of experience has shown that | only few programmers can discuss the advantages and | disadvantages of programming languages for certain | purposes in a halfway objective manner, and since few of | them implement their own languages even the good | discussions often remain fruitless. | | I'll recommend Reddit's r/ProgrammingLanguages, as well | as Scott's _Programming Language Pragmatics_ as a reading | instead. It 's not fully up-to-date but I found the | comparisons of features and their trade-offs in it | fascinating. | nyanpasu64 wrote: | I think Python suffers from #3, as they've been adding | new (controversial) syntax like typing, the := operator, | and pattern matching. These were intended to help write | real-world code at the cost of simplicity and | applicability as a teaching language. | [deleted] | miga wrote: | It is very confusing that equality in JavaScript is not | transitive: https://imgur.com/5pFXFbR. | | Apparently equality in Go is no longer transitive either... | nicoburns wrote: | JavaScript is fine so long as you stick to === rather than ==. | At work, == is completely banned and tjis is enforces by CI. | jerf wrote: | I 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/ to be | sure. Offhand I can't think of how to make one. | morelisp wrote: | There 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 wrote: | Thank you, I missed that. | cratermoon wrote: | It'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 wrote: | Pointers 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 wrote: | Much discussion about nil in Go just yesterday: | | https://news.ycombinator.com/item?id=26635529 ___________________________________________________________________ (page generated 2021-03-31 23:01 UTC)