Hacker News new | ask | show | jobs
by LudwigNagasena 691 days ago
> non-nilness

Ah, I still remember this thread:

https://groups.google.com/g/golang-nuts/c/rvGTZSFU8sY/m/R7El...

4 comments

Wow, that's painful to read.

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.

False things programmers believe:

All reference types should be able to take a null value.

It's impossible to write complex and performant programs without null.

It's impossible to write complex and performant programs without pointers.

References always hold a memory address in a linear address space. (Not even true in C!)

Every type is comparable.

Every type is printable.

Every type should derive from the same common type.

All primitive types should support all kind of arithmetic the language has operators for.

The only way to extend an existing type is to inherit from it.

What else?

Every type must have some sort of a default value. (A generalization of the first item, really.)
It's going to be much faster to enumerate the true things programmers believe.
> It's impossible to write complex and performant programs without pointers.

Well, I'd rather not copy around a multi-hundred-megabyte (or gigabyte) 3D object around to be able to poke its parts at will.

I'll also rather not copy its parts millions of times a second.

While not having pointers doesn't make impossible, it makes writing certain kinds of problems hard and cumbersome.

Even programming languages which do not have pointers (cough Java cough), carry pointers transparently prevent copying and performance hits.

Well, looks like the GP missed a very common false fact:

The operations written in a program must literally represent the operations the computer will execute.

This one stops being true on high-level languages at the level of x86 assembly.

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.

True. :)
Without pointers in some form or another, you can’t refer to allocated memory. You can change the name of pointers but they are still pointers.
It is possible to write complex and performant programs without allocating memory.

And in some languages, where you only operate on values, and never worry about where something is stored, allocation is just an implementation detail.

> 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.

That's... not true?

Example in C:

void fun(void) {

    int a[16];

    for (int i = 0; i < sizeof(a); i++)
    {
        a[i] = 1;
    }
}

I have allocated and referred to memory without pointers here.

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).

> Every type is printable.

It’s 2024, every type is jsonable!

Never met a programmer that thought these things were true.
A few of those myths are stated as fact in the aforementioned thread.
> 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.

This is exactly what Rust does, and as a result, Option<&T>, Option<Box<T>>, etc are guaranteed to have zero overhead: https://doc.rust-lang.org/std/option/index.html#representati...

> 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.

https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/cheri...

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)

> Wow, that's painful to read.

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.

Nullable pointers are fine for those who need them. What we're asking for is non-nullable pointers.
> "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.

[1] https://en.wikipedia.org/wiki/Flow-sensitive_typing

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.
What basic properties no longer hold?
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.
That would still hold. Those things just wouldn’t be typed as non-nullable.
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.

> An integer range is a very basic type, too

Not really, its semantics get hairy almost instantly. Eg does it incrementing it produce a new range?

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.
I regularly find quite smart people assume Rust's references must be fat pointers to handle lifetimes, and check them all at runtime.
> An integer range is a very basic type, too, conceptually

Just signed vs unsigned makes this a complex topic.

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.
It's like they don't speak the same languages.