proposal: Go 2: explicitly nillable / non-nillable types #30177
Labels
FrozenDueToAge
LanguageChange
Suggested changes to the Go language
Proposal
v2
An incompatible library change
Milestone
Background
In Go, we are locked into some types having a nil representation and some not. The general rule is the "pointery" types have nil values, but even this is somewhat self-contradictory at first glance with the
string
type; this could be seen as a kind of pointer, yet cannot be nil.This proposal is to give the developer more control over what types are nillable and what aren't, without adding much complexity to the simple type system.
Motivation
Explicitly nillable
A common use case of nilable types is databases. For example, we may have a string type in our DB that could be null, and want to express that in Go. There are two common solutions for this:
1) Use a struct
This allows us to distinguish between "nothing" and "empty" through the boolean, although falls down as it has limited static checking; anyone could set the Valid field to false, even when the Value field is non-empty. It also creates clutter in our codebase, as it needs to be rewritten for each type of Null field in our DB.
2) Use a pointer
The pointer type gives us a nil value like we want, and also comes with the static checking we want. Seems good so far. However, the pointer type also comes with all of the pointer semantics. This means we don't show our intent clearly; we could be showing a string that is passed around and modified, which we don't necessarily want. It feels like a hack.
Explicitly non-nillable
Sometimes, we never want certain nillable types to be nil. The most common, in my opinion, is maps. Sometimes we want it to always just contain data or be empty, we don't want it to actually "be" nothing.
The Proposal
I am proposing adding two new type qualifiers:
#
for non-nillable, and?
for nillable. These will be prefix qualifiers, similar to pointers.The following can be
?
qualified and made nillableAny type that cannot be
?
qualified can be#
qualified. These are:They can be nested in compound types, so they don't have to be only at the top level. However, multiple qualifiers of this kind can't be on the same level, as this would create strange corner cases, so is not worth allowing.
Examples:
Semantics
Nillable
Nillable types won't be far off what we know today - they can be thought of as changing the zero value of whatever it may be to nil.
Attempting to perform numeric operations on a numeric type that is nil will result in a runtime panic.
Example:
This checking may seem tedious, however that is more to do with the language itself and out of the scope of this proposal.
This propagates to any operation on a nil type, except method calling. In keeping with Go's traditions, calling a method on a nil type is not an error.
Non-Nillable
The idea of making types non-nillable is more nuanced than nillable. We are doing something that is new to Go entirely; we are removing the zero value.
As a result of this,
#
qualified types can't be initialised implicitly, and must be explicitly. This also means that a struct containing a#
qualified type can't be initialised implicitly either.Expressed formally, a composite type cannot be implicitly initialised if any of its component types have no zero value.
This is a hefty restriction on use of non-nillable types. However, I believe it is justified, because the guarantees we get for it are worth the cost (imo).
Examples:
Conversions
In keeping with Go, there will not be any implicit conversions between types.
#T -> T and T -> ?T
These conversions are lossless, so they will always succeed (as they are adding new information). Thus, they can be done at compile time using the normal conversion operator.
T -> #T and ?T -> T
These are lossy, so there needs to be runtime check that fails if the type is nil. For this, I propose two new operators:
?
and#
.?
Casts away the nilness of a value, and panics if the value is nil.#
Casts a value into non-nillable, and panics if the value is nilBoth of these will have "safe" variants using the
, ok
idiom.Examples:
Alternatives
There are some alternative stuff to consider that I have left to the end for clarity.
Choice of prefix, not postfix
We could use postfix qualifiers instead of prefix, like most other languages. (e.g.
int?
instead of?int
)However, I chose not to opt for the prefix because
[3]int?
a nillable array of int or an array of nillable int)Use of
#
instead of!
One might expect the use of
!
like other languages. However, it creates an ambiguity with the logical not operator, which is unneccessary.Choice to remove zero values
An alternative approach to non-nillable types would be to create a new zero values to replace nil. Slice would have the empty slice and map would have the empty map.
However, for pointer values this would not work without allowing dangling pointers.
Therefore, for this approach to work, we would have to disallow non nillable pointers, which are a major use case for this proposal.
Conversion operators
The conversion operators are an interesting idea that I am not 100% confident in, although it seems to me to be the simplest and cleanest approach to it. An alternative would be some built in function.
Conclusion
I believe this proposal will improve the usability and expressivity of the language, outweighing the potential complexity increase.
The text was updated successfully, but these errors were encountered: