Separating the concept of pointers and nullable types is one of the things that I think go having from the beginning would have made it a much better language. Generics and sum types are a couple of others.
Exactly. A MOV is reduced to a register rename. An intelligent compiler can rewrite multiply/divide by 2 as shifts if it makes sense, etc.
"Assembly is not a low level language" is my favorite take, and with microcode and all the magic inside the CPU, it becomes higher level at every iteration.
> It is possible to write complex and performant programs without allocating memory.
I assume you mean by only allocating on the stack? Those are still allocations. It's just someone else doing it for you.
> And in some languages, where you only operate on values, and never worry about where something is stored, allocation is just an implementation detail.
Again, that's someone else deciding what to allocate where and how to handle the pointers etc. Don't get me wrong, I very much appreciate FP, as long as I do information processing, but alot of programming doesn't deal in abstract values but in actual memory, for example functional programming language compilers.
in some form or another is the key to their point.
Here's something to try at home .. exactly your code save for this change:
i[a] = 1;
... guess what, still compiles, still works !!
WTF ??? you ask, well, you see, X[Y] is just syntactic sugar for X+Y - it's a pointer operation disguised to look like a rose (but it smells just the same).
> It's impossible to write complex and performant programs without null.
Well, clearly there is a need for a special value that is not part of the set of legal values. Things like std::optional etc. are of course less performant.
If I can dream, all of this would be solved by 72-bit CPUs, which would be the same as 64-bit CPUs, but the upper 8 bits can be used for garbage collection tags, sentinel values, option types etc.
> Well, clearly there is a need for a special value that is not part of the set of legal values.
There's a neat trick available here: If you make zero an illegal value for the pointer itself, you can use zero as your "special value" for the std::optional wrapper, and the performance overhead goes away.
> If I can dream, all of this would be solved by 72-bit CPUs, which would be the same as 64-bit CPUs, but the upper 8 bits can be used for garbage collection tags, sentinel values, option types etc.
Address space is 64bit, pointers are 128bit, and encode the region the pointer is allowed to dereference. And there's a secret 129th bit that doesn't live in the address space that gets flipped if the pointer is overwritten (unless it's an explicit instruction for changing a pointer)
And the dismissive tone of some people including Ian. But to be fair before Rust there was definitely this widespread myth in the dev hivemind that nullable pointers is just the cost of performance and low level control. What’s fascinating is how easy and hindsight-obvious it was to rid code of them. I’ve never had to use pointers in Rust and I’ve worked on quite advanced stuff.
> "Go doesn't have nullable types in general. We haven't seen a real desire for them"
Ouch, who were they asking? There are so many problems from even the most simple CRUD apps where "lack of a value" must be modelled, but where the zero-value is a valid value and therefore an unsuitable substitute. This is probably my single biggest pain point with Go.
Using pointers to model nullability, or other "hacks" like using a map where keys may not be set, feel completely at odds with Go's stated goal of high code clarity and its general disdain for trickery.
I know with generics it's now trivially easy to implement your own Optional wrappers, but the fact that it's not part of the language or even the standard library means you're never going to have a universal way of modelling this incredibly basic and common requirement across projects. It also means you're never going to have any compile-time guarantees against not accidentally using an invalid value—though that's also the case with the ubiquitous (value, error) pattern and so is evidently not something the language is concerned with.
Everyone just keeps repeating the same old gripe, without bothering to read the responses.
Go needs a null-like thing because the language forces every type to have a zero value. To remove the concept of zero value from Go would be a major change.
The responses from Ian and the Go fans are not very well-thought.
To begin with, zero values were never a great idea. It sounds better than what C does (undefined behavior), but zero values can also hide subtle bugs. The correct approach is to force values to always be initialized on declaration or make use-before-initialization an error.
Having said that, it was probably too late to fix zero values by 2009, when Go was released to the public, and this is not what the thread's OP suggested. He referred to Eiffel, which is an old language from the 1990s (at least?) that didn't initially have null-safety (or "void-safety" in Eiffel's case), but released a mechanism to do just that in 2009, shortly after Tony Hoare's talk at QCon London 2009 (no idea if they were influenced by the talk, but they did mention the "Billion Dollar Mistake" in the release notes).
Eiffel's added nullability and non-nullability markers to types (called "detachable" and "attached"), but it's also using flow-sensitive typing[1] to prevent null-dereferencing (which is the main cause for bugs).
The thread OP didn't ask to eliminate zero values or nullable types, but rather requested to have a non-nullable pointer type, and flow-sensitive typing.
If structs need to be zero-initialized, a non-nullable pointer could be forbidden in structs, or alternatively Go could make explicit initialization mandatory for structs that have non-nullable pointers. At the very least, Go could support non-nullable pointers as local stack values, and use flow-sensitive typing to prevent null dereference.
If there's a non-nullable type, then there's types without zero values, and that means some basic properties of Go no longer hold. I don't know how many times that can be said differently. Whether something is in a struct or not is not relevant.
Uninitialized variables are zero. Composite literals may omit fields, and they'll be zero. Map accesses for nonexistent keys return zero values. Channel receives from closed channels return zero values. make returns zero-valued slices. Comma-ok style type assertions return zero values. Slices are fat pointers where the zero value avoids an allocation for data.
Wow, that discussion is infuriating. I'm shocked that many people on there don't seem to understand the difference between compile time checks and runtime checks, or the very basics of type systems.
I think people do understand the basics of static type systems, but disagree about which types are essential in a "system language" (whatever that is).
An integer range is a very basic type, too, conceptually, but many languages don't support them in the type system. You get an unsigned int type if you're lucky.
The semantics are always complex. The same type of question arises for all basic types. For example, what does adding a string to an integer produce?
Or do you give up on answering that and simply prevent adding strings and integers? When one wants to add them they can first manually apply an appropriate type conversion.
That is certainly a valid way to address your question – i.e. don't allow incrementing said type. Force converting it to a type that supports incrementing, and then from that the developer can, if they so choose, convert it back to an appropriate range type, including the original range type if suitable.
Of course, different languages will have different opinions about what is the "right" answer to these questions.
I think you're confusing the type and value level.
The original statement was about a range type, that is something like an integer that is statically constrained to a range of, say, 1..4 (1, 2, 3, 4).
To work with this as a type you need to have type level operations, such as adding two ranges (which can yield a disjoint range!), adding elements to the range, and so on, which produce new types. These all have to work on types, not on values. If 1..4 + 5..8 = 1..8 this has to happen at the type level, or, in other words, at compile-time.
Range types are very complicated types, compared to the types most people deal with.
Converting a string to an int is very simple to type (String => Int if you ignore errors) and adding integers is also simple to type ((Int, Int) => Int)
A range type could be very simple if it were just used for storage - you couldn’t do anything with it other than passing it around and converting it to something else, and there would be a runtime check when creating it.
But such a thing would be useful mostly for fields in data structures, and the runtime checks would add overhead. (Though, perhaps it would replace an array bounds check somewhere else?)
OP is just saying that you don't have to permit operations such as addition or incrementation on range types, in which case you don't need the corresponding type-level operations.
Many people on here as well! :-) Reading the comments on this post is stepping into an alternative universe from the PL crowd I usually interact with. Very conservative. It's quite interesting.
Separating the concept of pointers and nullable types is one of the things that I think go having from the beginning would have made it a much better language. Generics and sum types are a couple of others.