5 ms·
I havent followed this argument closely so forgive me if I'm missing relevant discussion, but I dont see why the Rust style isnt just adopted. Its the thing I i
by JamesSwift 1y ago
I havent followed this argument closely so forgive me if I'm missing relevant discussion, but I dont see why the Rust style isnt just adopted. Its the thing I immediately add now that I have generics in Go.
I only see this blurb in a linked article:
> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.
But I fail to see how having convenience equates to ignoring the error. Thats basically half of my problem with Go's approach, that nothing enforces anything about the result and only minimally enforces checking the error. eg this results in 'declared and not used: err'
x, err := strconv.Atoi("123")
fmt.Println("result:", x)
but this runs just fine (and you will have no idea because of the default 0 value for `y`):
x, err := strconv.Atoi("123")
if err != nil {
panic(err)
}
y, err := strconv.Atoi("1234")
fmt.Println("result:", x, y)
this also compiles and runs just fine but again you would have no idea something was wrong
x, err := strconv.Atoi("123")
if err != nil {
}
fmt.Println("result:", x)
Making the return be `result` _enforces_ that you have to make a decision. Who cares if someone yolos a `!` or conveniently uses `?` but doesnt handle the error case. Are you going to forbid `panic` too?
- School-Cotton 1y agoGo can’t have Result because they don’t have sum types, and they can’t add them because of their bizarre insistence that every type has to have a designated zero value.
- masklinn 1y ago> they can’t add them because of their bizarre insistence that every type has to have a designated zero value. Nothing prevents adding union types with a zero value. Sure it sucks, but so do universal zero values in pretty much every other situation so that's not really a change.
- School-Cotton 1y agoMaking it so all sum types have to be nillable would make them dramatically worse (the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL). I guess this is in agreement with your point.
- masklinn 1y ago> the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL I don't think that's the case in Go: whereas I got the impression the C# team started souring on default() after generics landed (possibly because nullable value types landed alongside and they found out that worked just fine and there was no reason nullable reference types wouldn't) I don't really get that impression from the Go team, even less so from them still mostly being Googlers (proto3 removed both required fields and explicit default values).
- ndriscoll 1y agoIn Scala basically all types are nullable (including Option/Try/Either), but it never really comes up because no one uses nulls (except when wrapping java code) because the language makes it easier to just do the right thing almost all of the time. In fact the lack of sum types seems to be why you need everything to have a zero value in the first place: because sums are products you need a nonsensical value when the sum is the other type.
- tubthumper8 1y agoI've read elsewhere that one idea for the zero value of sum types is not making it nillable, but rather making the zero value as the 0th variant of the sum type (and if it has associated data, the zero value of that as well). It's weird, but does align with design decisions that have already been made. So if there was an `Option[T]` with variants `None` and `Some[T]`, the zero value would be `None` because that's the zero-th variant
- stouset 1y ago> every type has to have a designated zero value This bonkers design decision is, as far as I can tell, the underlying infectious cause of nearly every real issue with the language.
- 9rx 1y ago> I dont see why the Rust style isnt just adopted. Mostly because it is not entirely clear what the Rust-style equivalent in Go might be. What would Rust's "From" look like, for example?
- JamesSwift 1y agoSorry, I wasnt specific in that part. When I say 'rust style' Im really just referring to a union type of `result | error`, with a way to check the state (eg isError and isResult) along with a way to get the state (eg getResult and getError). Optionally '?' and '!' as sugar. That said, the other responder points out why the sum type approach is not favored (which is news to me, since like I said I havent followed the discussion)
- 9rx 1y agoIn Go, values are to be always useful, so `result | error` would be logically incorrect. `(result, result | error)`, perhaps – assuming Go had sum types, but that's a bit strange to pass the result twice. Just more of the pitfalls of it not being clear how Rust-style applies to an entirely different language with an entirely different view of the world.
- JamesSwift 1y agoWhat is useful about the value of `x` in the following code? x, err := strconv.Atoi("this is invalid") On the contrary, `x` is _lying_ to you about being useful and you have absolutely no idea if the string was "0" or "not zero"
- deleted 1y ago[deleted]
- 9rx 1y ago> and you have absolutely no idea if the string was "0" or "not zero" You do – err will tell you. But in practice, how often do you really care? As Go prescribes "Make the zero value useful" your code will be written in such a way that "0" is what you'll end up using downstream anyway, so most of the time it makes no difference. When it does, err is there to use. That might not make sense in other languages, but you must remember that they are other languages that see the world differently. Languages are about more than syntax – they encompass a whole way of thinking about programs.
- abtinf 1y ago> Its the thing I immediately add now that I have generics in Go. If you’re willing to share, I’m very curious to see a code example of what you mean by this.
- JamesSwift 1y agoThis is from a while back but was the first thing I thought of: https://gist.github.com/J-Swift/96cde097cc324de1f8e899ba30a16f9f https://gist.github.com/J-Swift/96cde097cc324de1f8e899ba30a1... I ripped most of it off of someone else, link in the gist
- jitl 1y agoMoving every success value to the heap seems like a big loss to me but I don't see an alternative. I think going the interface route also ends up wrapping everything in an even fatter pointer. But at least I get to think "ah maybe this isn't going to get boxed and it will be free". interface Result[T] { IsOk(): bool IsErr(): bool Unwrap(): T UnwrapError(): error } // Ok is a Result that represents a successful operation. struct Ok[T] { Value: T } func Ok[T](value T) Result[T] { return Ok[T]{Value: value} } func (s Ok[T]) IsOk() bool { return true } func (s Ok[T]) IsErr() bool { return false } func (s Ok[T]) Unwrap() T { return s.Value } func (s Ok[T]) UnwrapError() error { panic("UnwrapError called on Ok") } // Err is a Result that represents a failed operation. struct Err[T] { Reason: error } func Err[T](reason error) Result[T] { return Err[T]{Reason: reason} } func (e Err[T]) Error() string { return e.Reason.Error() } func (e Err[T]) IsOk() bool { return false } func (e Err[T]) IsErr() bool { return true } func (e Err[T]) Unwrap() T { panic(fmt.Errorf("Unwrap called on Err: %w", e.Reason)) } func (e Err[T]) UnwrapError() error { return e.Reason }
- masklinn 1y ago> But I fail to see how having convenience equates to ignoring the error. The convenience of writing `?` means nobody will bother wrapping errors anymore. Is what I understand of this extremely dubious argument. Since you could just design your `?` to encourage wrapping instead.
- NobodyNada 1y ago> Since you could just design your `?` to encourage wrapping instead. Which is exactly what Rust does -- if the error returned by the function does not match the error type of `?` expression, but the error can be converted using the `From` trait, then the conversion is automatically performed. You can write out the conversion implementation manually, or derive it with a crate like thiserror: #[derive(Error)] enum MyError { #[error("Failed to read file") IoError(#[from] std::io::Error) // ... } fn foo() -> Result<(), MyError> { let data = std::fs::read("/some/file")?; // ... } You can also use helper methods on Result (like `map_err`) for inserting explicit conversions between error types: fn foo() -> Result<(), MyError> { let data = std::fs::read("/some/file").map_err(MyError::IoError)?; // ... }
- masklinn 1y ago1. That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping. 2. Idiomatic go type erases errors, so you're converting from `error` to `error`, hence type-directed conversions are not even remotely an option.
- alexchamberlain 1y ago> That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping. In practice, the error type will be defined quite close to where the conversion is applied, so the static nature of it doesn’t feel too big.
- NobodyNada 1y ago
- Pxtl 1y agoWait... x, err := strconv.Atoi("123") if err != nil { panic(err) } y, err := strconv.Atoi("1234") fmt.Println("result:", x, y) > this also compiles and runs just fine but again you would have no idea something was wrong Okay, I don't use golang... but I thought ":=" was "single statement declare-and-assign". Is it not redeclaring "err" in your example on line 5, and therefore the new "err" variable (that would shadow the old err variable) should be considered unused and fail with 'declared and not used: err' Or does := just do vanilla assignment if the variable already exists?
- JamesSwift 1y agoAs I understand it, go has some special handling for this scenario because its so prevalent which special cases reassignment. The linked article touches on it > There are exceptions to this rule in areas with high “foot traffic”: assignments come to mind. Ironically, the ability to redeclare a variable in short variable declarations (:=) was introduced to address a problem that arose because of error handling: without redeclarations, sequences of error checks require a differently named err variable for each check (or additional separate variable declarations)
- Pxtl 1y ago... I thought Go's whole deal was that you give up the expressiveness and power of overdesigned languages for simple, clean, "only one way to do it" semantics. That "special cases reassignment" where ':=' is sometimes a shadowing declaration and sometimes a reassignment sounds like the opposite of that.
- homebrewer 1y agoThe language is full of gotchas like that, you're expected to use the tooling to guardrail yourself, because having a proper type system or coherent syntax is "too complicated" (while learning dozens of patterns and weird tricks apparently isn't). go vet and this massive collection of linters bundled into a single binary are very popular: https://golangci-lint.run https://golangci-lint.run linters will warn you of accidental shadowing, among many other things.
- mseepgood 1y ago- It has poor visibility, it hides control flow branches in a single statement / expression. That's one of the reasons Go got rid of the ternary operator in favor of an if statement where each branch has to be on its own line. - It isn't easily breakpointable. - It favors "bubbling up" as-is over enriching or handling.