So let's say I'm making a type, PrimeNumber, and I have a method that validates a given number is prime and returns an error if not. In Rust, that's something like
If you let `PrimeNumber` have an arbitrary zero value, you've lost the guarantee that all `PrimeNumber` values are prime.
Rust has tons of these, e.g. NonZeroU64 is guaranteed to be >=1. If you are somehow holding one, you don't have to check.
You can e.g. make a `struct Username(String)` and all paths to creating one enforce it to match a grammar; you will never encounter a `Username` that's the empty string, or contains non-printing characters, etc.
If you think of solving that by having a way for PrimeNumber to say "my zero value is actually 2", well, a) that's really weird and will just cause different bugs and b) that only solves the very simplest case.
Sorry, but I don't follow. Practically, in the Go design above, PrimeNumber is going to be uninitialized only if the current value of Result is PrimeError. Which can be checked with type matching: which value is active. Attempting to dereference an uninitialized option would panic, we just shifted it from compile-time to runtime (which is typical of Go). I don't understand how things like "NonZeroU64 is guaranteed to be >=1" are related to the notion of sum types per se. Sure Go has a weaker type system than Rust and you can't guarantee that a struct will never be zero-initialized, but you can enforce it with conventions, like wrapping values with structs, using constructor functions, etc.
Example (with proposed syntax):
type Username struct {
value string
}
func NewUsername(value string) option[Username, UsernameError] {
if value == "" {
return make(option[Username, UsernameError], NewUsernameError(..))
}
return make(option[Username, UsernameError], Username{value: value})
}
u := NewUsername("")
switch u.(type) {
case Username:
// ...
case UsernameError:
// ..
}
Verbose and non-idiomatic but seems to work. Or I don't understand something.
The idea of types that can only contain "approved" values is the point of sum types for error handling. You either get an error, or a valid value, and there is no way to manufacture a zero value of Username out of thin air.
If you don't have that, you're not really improving on `value, ok := foo()`.
>there is no way to manufacture a zero value of Username out of thin air
As I said, with the proposal above, you cannot manufacture a default zero value from a union if you specify in the language's spec that trying to retrieve a value from a non-initialized ("not active") subtype panics (i.e. the union was constructed with a different underlying type)
asUsername := myUnion.(UserName) // panics if myUnion doesn't hold UserName internally
// if you want to retrieve the value, you type-switch:
switch myUnion.(type) {
case UserName:
// if the underlying type is UserName, it always returns the value as it was passed into the union's constructor by the developer, it doesn't matter if it was "initialized" or the developer passed some default zero'd value; it's not the union's concern at all
}
It's similar to panicking on nil interfaces, nil maps etc. The article is called "Good union types in Go would probably need types without a zero value", so I responded to that -- you don't need types without a zero value for the use case of union types. It's also different from interfaces (as you noted earlier), because this allows disjoint types.
Whether Go must allow non-zero types in general is a totally different topic.