Remix.run Logo
tialaramex 5 hours ago

Both languages don't do much about Tony Hoare's Billion Dollar Mistake, and to me that's an immediate black mark on type safety, though they fail in this regard in distinct ways.

In Java all of your user defined types are, alas, represented only referentially and a null reference is always possible, so even if there is no such thing as an invalid Goose, your variable of type "Goose" may just be null anyway.

In Go, it's possible to actually make a Goose and have it as just a local variable living on your stack, but, the language insists that anybody can conjure one into existence despite having no basis for doing so, the resulting Goose is at best now in some invalid "null" state.

To me, avoiding the Billion Dollar Mistake in new software is table stakes. Even at work, in the relatively boring C# language, we can avert the Billion Dollar Mistake in new codebases and gradually wean old ones off this idea. The CLR is hostile to properly fixing the mistake eco-system wide, but this is a marked improvement.

cogman10 5 hours ago | parent | next [-]

> Both languages don't do much about Tony Hoare's Billion Dollar Mistake, and to me that's an immediate black mark on type safety, though they fail in this regard in distinct ways.

Java is well on its way to alleviating the problem. [1]

[1] https://openjdk.org/jeps/8303099

imbnwa 5 hours ago | parent [-]

They really ruined Optional not solving this then and there

cogman10 5 hours ago | parent [-]

There would always be a hole that's somewhat impossible to fill with the language as is.

If you have something like

    Map<Integer, Optional<Integer>> map;
    var foo = map.get(1);
if `1` isn't present in the map then `foo` has to be set to something. It will be `null` in this case.

If the new null restricted stuff makes it in, you can express this as

    Map<Integer, Optional<Integer>!> map;
    var foo = map.get(1);
and yet, `foo` will still end up being `null` in that case (I assume) because something has to come back if `1` isn't present.

At best, that will prevent you from doing `map.put(1, null);`

cobbal 5 hours ago | parent | next [-]

I'm not completely sure what this example is trying to illustrate. Most languages with proper sum types would have `Map<K, V>.get` return an `Optional<V>`. If V happens to be `Optional<Integer>`, then the result will be an `Optional<Optional<Integer>>`, which is a perfectly good type that represents the two ways a value may be missing from the map.

Am I missing something?

pbh101 4 hours ago | parent [-]

The Map interface pre-dates Optional and does not return Optional, so it won't return an empty Optional but rather null.

To be more clear:

    Map<K, V>.get(foo)
returns a `V`, not an `Optional<V>`. If your `V` is `Optional<WrappedV>`, cool, but that doesn't change that `Map` either finds or doesn't find a V.
stickfigure 4 hours ago | parent [-]

The parent certainly knows this. Additional methods could be added to `Map` which return `Optional`. I am annoyed by their absence every day.

thrw42A8N 5 hours ago | parent | prev | next [-]

The actual point is so that the compiler can tell you that you forgot to check all possible cases. Typescript is really great for this.

5 hours ago | parent | prev [-]
[deleted]
brundolf 4 hours ago | parent | prev | next [-]

This is my #1 issue with Go, by a mile. If it weren't for this I'd probably consider using it for projects, but with this I can't conscionably do so

nordsieck 5 hours ago | parent | prev | next [-]

> In Go, it's possible to actually make a Goose and have it as just a local variable living on your stack, but, the language insists that anybody can conjure one into existence despite having no basis for doing so, the resulting Goose is at best now in some invalid "null" state.

If you really don't want people instantiating instances of a type on their own, can't you just make the type private but make a factory for the type public?

jerf 4 hours ago | parent | next [-]

"If you really don't want people instantiating instances of a type on their own, can't you just make the type private but make a factory for the type public?"

"Yes", but really, no. It is syntactically valid to write a function that returns an unexported type, and you can technically get an instance of that unexported type in your other package by using := to assign it to a variable. However, you can not name the type in the other packages at all. So, you can not say "var x mypkg.unexportedType", even if you can "x := mypkg.ReturnUnexportedType()". You can not put it in a struct/channel/slice/map/whatever, since you can't name it and there's no equivalent of := for types. You can't refactor the function that contains the "x" in any way that involves passing it to a new function because the type signature for the new function would have to mention the unexported type, which it can't do, and there is no inference for function types that would let you elide it. Reflect restrictions on unexported types are still in effect so even trying to put it in an "any" isn't really all that useful.

Basically, it's useless, even though it's just barely sort of valid, because it's much more than just creation that is blocked. In practice you have to export types you want users to be able to assign to, put in structs, etc., and if they are exported, their zero values can be created by any package that imports them. One of the standard linters in golangci-lint will warn you if you accidentally write an unexported type into an exported type or value, because you didn't mean that, even if you thought you did.

You can write an interface that you export, and then return an unexported data type that conforms to that interface. However, you then can't prevent someone from having a nil instance of that interface, so in terms of preventing invalid data, this doesn't really do anything useful. This is more useful for manipulating package documentation by not exporting types unnecessarily and having to write docs for them (or, if you don't write docs, having them appear in the godoc) then as access control.

stouset 4 hours ago | parent | prev | next [-]

Dangerous by default is still a pretty bad design choice.

evantbyrne 4 hours ago | parent [-]

There is no default. Functions, types, methods, and fields are either public or private based on capitalization.

actionfromafar 4 hours ago | parent | prev [-]

But the point is Go still allows bad patterns. You can always avoid bad patterns, but not everyone will. See C++20.

Perfectly possible to write beautiful code. Alas, hell is other coders, or something.

Edit hot take: I wish Go would just have been Pascal/Delphi rebranded. All language discussions just ignore we had a contender against C for decades.

If you say it’s too weird, don’t come dragging in Go in the same breath, thanks.

release-object 4 hours ago | parent | prev | next [-]

> the language insists that anybody can conjure one into existence despite having no basis for doing so

You can have a goose (unepxorted) that is created via NewGoose() (exported - really a constructor). Of course within a package there is nothing to stop you doing the wrong thing. I really like Go. But I do find myself occasionally writing overly verbose types just to avoid this issue. Which, if I’m honest with myself, is a bit silly.

There’s a good counter argument to using to unepxorted types like this below.

Shortcut: https://news.ycombinator.com/item?id=42250614

jajko 5 hours ago | parent | prev [-]

I wouldn't put that much drama into nulls, these are mostly academic discussions. In real world its much less drama in well maintained codebases, once you learn working with them you don't even actively think about it. In the meantime, basically all companies run on it, and will do so for next 50 years minimum, plus you have what... 17 million devs working with it?

Defensive programming is generally a good approach regardless of domain or language used, and if used well you don't have to worry about nulls in Java, at least not in your own code.

brundolf 4 hours ago | parent | next [-]

I worked at a Java shop for a while, with a small and new-ish codebase built by experienced devs, and most of the errors we encountered were null pointer exceptions. It was always just, "alright where do we need to add another if-statement this time"

Of course you could be "defensive" by adding an if-statement every single time you use any object. But that would mean an order of magnitude more lines of code, and even then it would be easy to miss cases

lolinder 4 hours ago | parent | prev | next [-]

Every thread about null has someone come in and make this claim that it doesn't come up in production in well-maintained codebases, but it feels very No True Scotsman: every counterexample that someone can point to immediately doesn't count, because the definition of a "well-maintained codebase" seems to be one in which null pointers aren't an issue.

I guess that's a fine definition in theory, but every codebase that I've ever worked in that was written in a language with null references has had null pointer exceptions as one of the top recurring bugs. Maybe that means all my codebases have been poorly maintained, but if so that's a very good reason to design languages that require less maintenance.

tshaddox 4 hours ago | parent | prev | next [-]

Of course it's physically possible for a software project to exist where all contributors flawlessly handle nullability.

The point is rather that, because this can be automated, it ought to be automated.

The same could be said of automated tests ("in well-maintained codebases contributors manually run through a large suite of tests to ensure there are no regressions.")

stouset 4 hours ago | parent | prev | next [-]

This is the “as long as programmers are perfect and remember to always do the right thing everywhere, everything will be great” argument.

Developers are not perfect and the footguns in languages have a cumulative effect. Any one thing isn’t necessarily a big deal but when you have multiple interacting footguns (always check for nil, nil interfaces don’t actually equal nil, default zero values need to be sane even when they make no sense, etc.) both the surface area for errors and the level of care needed to avoid them increases dramatically.

vlovich123 4 hours ago | parent | prev | next [-]

Do you have null dereference bugs that you occasionally solve, even if only during development? The "billion dollar mistake" is a reference to the economic cost of all bugs it created as a concept. That doesn't mean you don't build techniques to work around it, but it does mean that that's the economic efficiency left on the floor.

In other words, the cognitive overhead you spend on worrying about nulls (however you do that), programmers in other languages spend on other things.

discreteevent 4 hours ago | parent [-]

The reason Hoare used the word billion was to emphasize how costly it was compared to other things. That was certainly true in C code 40 years ago and more. Those bugs were hard to find and hard to fix. In java you get a full stack trace, it's quick to fix and it doesn't come up often. Like the OP said - In practice it's not really an issue. "Worrying about nulls" is not something that most programmers do.

vlovich123 2 hours ago | parent [-]

In C/C++ you could argue that's the case too. What's your point? My point is that fixing those bugs still costs time. It's come down in price as tooling has gotten better but the problems of a null reference remain as relevant today in languages that still use them.

skitter 3 hours ago | parent | prev [-]

Finding out a piece of software I'm using is written in Go because crashes with SIGSEGV does not spark joy.