3 ms·
Go already has pseudo-functions such as "make", special syntax for channels/map, and switching on types, technically they could do something like this: valu
by kgeist 2y ago
Go already has pseudo-functions such as "make", special syntax for channels/map, and switching on types, technically they could do something like this:
value := make(option[int, bool], true)
b := value.(bool) // panic if not a bool
switch value.(type) {
case int:
case bool:
}
If an option value isn't initialized, all attempts to do something with such an option will panic (there's already such a rule for nil interfaces). Not saying it's good design, but it's consistent with the syntax/semantics of the rest of the language.
- yencabulator 2y agoBut then you lose all of the benefits of sum types, so what's the point? You can do roughly what you're asking today with interfaces.
- kgeist 2y agoWhat are the benefits? You mean exhaustive match or...?
- yencabulator 2y agoSo 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 pub struct PrimeNumber(u64) impl PrimeNumber { pub fn new(n: u64) -> Result<PrimeNumber, PrimeError> { ... } } 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.
- kgeist 2y agoSorry, 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.
- yencabulator 2y agoThe 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()`.
- kgeist 2y ago>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.