6 ms·
Emulating sum types in languages without pattern matching is extremely awkward, to the point of being almost useless. type Result[T] struct { ok: *T err: e
by the_duke 5y ago
Emulating sum types in languages without pattern matching is extremely awkward, to the point of being almost useless.
type Result[T] struct {
ok: *T
err: error
}
Great, so how do you work with it?
* You can have a `ok()` getter that returns `*T`. Now you you need an `if x != nil`
* `isOk()` + `isErr()`
* `unwrap() T`, which panics on errors
* `split() (*T, err)` that splits into separate values, especially awkward since you both need `if err != nil` AND dereference the pointer
That API is more awkward then the status quo, and doesn't buy you any correctness guarantees because eventually you have to do an `if x := res.ok(); x != nil` or `x, err := res.split(); if err != nil {` anyway.
Pretty much the only convenience you gain are functions like `map()` or `unwrap_or`, but eventually you always have to take out the value and without being able to pattern match you can't get an API that improves correctness much.
(std::optional in C++ is a great example)
- kbd 5y ago> because eventually you have to do... Emphasis on eventually instead of at every single function call.
- linkdd 5y agoThe Rust Result type has: - map / map_err - and_then / or_else - unwrap / unwrap_or And countless of other functions making it very practical to chain computations without having to pattern match anything. I do the same in Erlang/Elixir. In Golang, I need to check every function call, and if I want to know where an error come from, I need to wrap it in an errors.New() because no exceptions = no stacktrace
- kbd 5y ago> and if I want to know where an error come from, I need to wrap it in an errors.New() because no exceptions = no stacktrace Zig manages to provide traces with very little overhead: https://ziglang.org/documentation/master/#Error-Return-Traces https://ziglang.org/documentation/master/#Error-Return-Trace... I am baffled as to why error handling in Go remains so impoverished.
- egeozcan 5y agoI don't see why that wouldn't be possible to implement now that the generics are here.
- linkdd 5y agoWhich is exactly why I said: > I cannot wait for the Result monad. Generics make this possible, and will be a huge improvement to Go's error handling. I'm not saying it will solve everything, but it's a huge step nonetheless.
- egeozcan 5y agoI seem to have lost track of how the discussion reached that point, thanks for the clarification.
- xyzzy_plugh 5y agoI think you will find that Go's error handling will not change as much as you suggest. The majority of developers are extremely cautious of generics and found the status quo to be the best balance between readability and conciseness. Convenience is not a goal of the language, generally.
- Aeolun 5y agoIt doesn’t need to be part of the core language. Everyone can make their own monad, or use the one from a library.
- saghm 5y ago> The majority of developers are extremely cautious of generics and found the status quo to be the best balance between readability and conciseness. Most people who use Go have specifically chosen to use it though (or at least sought out opportunities, given that it's not something entrenched like Java being used due to inertia), so yes, most of them are fine with how Go is or they wouldn't be using it in the first place. If Go had generics from the start, I'd imagine most users would be fine with that too due to self-selection.
- 5y ago
- DarylZero 5y agoIn general, you can replace pattern matching with a function that takes one function for each constructor of the sum type. However, doing this kind of ... sucks.
- eru 5y agoIt sucks when you have to use it for everything, but it's a useful enough technique that most Haskell datatypes come with functions that provide exactly this conversion to church encoding. See eg 'maybe' (https://www.stackage.org/haddock/lts-18.18/base-4.14.3.0/Prelude.html#v:maybe https://www.stackage.org/haddock/lts-18.18/base-4.14.3.0/Pre... on stackage) or foldr for lists. 'foldr' is interesting, because it encapsulates a recursive pattern matching on lists. For the non-recursive version, see 'uncons' composed with 'maybe'.
- DarylZero 5y agoIt's probably actually better than pattern matching when there are only two, as in those cases. But even at three, it becomes a mess. And more than three? Yuck.
- eru 5y agoIf your language does currying similar to Haskell, and there's a good order to your constructors / arguments, even with three or more there might be some benefits with partial application.
- johnisgood 5y agoHuge no to anything that will require .unwrap() noise everywhere in the codebase. I use Erlang, my code does not have ".unwrap().unwrap()"[1] anywhere. [1] https://github.com/SeaQL/sea-orm/blob/64c54f8ad603df0c1d9da85ce7b0d9b59dc32fe4/examples/rocket_example/src/main.rs#L56 https://github.com/SeaQL/sea-orm/blob/64c54f8ad603df0c1d9da8...
- imron 5y agoI use rust and my code does not have .unwrap() (and definitely not .unwrap().unwrap()) in it either. That's what the ? operator is for.
- eru 5y agoThe '?' is not a conceptual advance over .unwrap(), it's "just" a major syntactic convenience.
- ComputerGuru 5y agoOf course it is. Unwrap panics on error, while ? bubbles up the error instead. But ? wasn’t introduced to replace unwrap, unwrap was always frowned upon. It instead replaces match and return.
- imron 5y agoIt was always frowned upon, but still ended up everywhere (including lots of documentation and example code) because it was less verbose than the match handling. The ? helped remove that last obstacle.
- TheDong 5y agoThe comparison here is with go. Unwrap would look like: file := os.Open("foo").Unwrap() I'll take that over the current go state of the art: file, err := os.Open("foo") if err != nil { panic(err) } Just like "panic(err)" is used infrequently, "unwrap" would be used infrequently. They're comparable, and for the cases where unwrap is okay (test code, once-off scripts, etc), I'd definitely prefer it to the panic boilerplate.
- preseinger 5y agoChaining computations hides the error control flow path, which negatively impacts readability.
- mariusor 5y agoStack traces exist in go[1], I'm not sure why you consider exceptions to be the only way of getting a stack representation. Not to mention that traditional exception handling advice I've been handed down from the gray beards is to always handle exceptions as early as possible, which is exactly what go forces you to do with their approach. [1] https://pkg.go.dev/runtime/debug#Stack https://pkg.go.dev/runtime/debug#Stack
- peoplefromibiza 5y agoyou can map on ok which is an alias for map_ok and then map_err for errors and or_else or_throw etc. that's similar to what Java does with the Optional type, not great, but not bad either the alternative is checking for nulls which is worse in any possibile way I usually implement something like Kotlin Result when I have to code in Java with a couple of static helpers to build the result: Result.success(T) Result.failure(Throwable t) https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/ https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/ also Result in Rust has been an inspiration https://doc.rust-lang.org/std/result/enum.Result.html https://doc.rust-lang.org/std/result/enum.Result.html
- linkdd 5y agoI also made a library for working with `{:ok, value}` and `{:error, reason}` in Elixir: https://github.com/linkdd/rustic_result https://github.com/linkdd/rustic_result Thanks to the pipeline operator and pattern matching, it makes pretty easy to read pipelines. It does not completely replace the with statement (that was not the point) but it simplified a lot of code.
- peoplefromibiza 5y agothis is nice, especially for the function clauses Elixir is my daily drive and working in Java made me miserable until I started working around the lack of pattern matching facilities Error handling is the worst part, I think a simple switch (retval) { Ok(val): ... break; Error(err): ... would make Java much more pleasant, even without full pattern matching everywhere.
- crdrost 5y agoThis sorta exists in Java 17: sealed interface Result<T> permits Ok, Error {} record Ok<T>(value: T) implements Result<T> {} record Error<T>(error: Throwable) implements Result<T> {} // to consume switch (result) { case Error e -> e.error().getMessage(), case Ok v -> v.value().toString() }
- 5y ago
- valenterry 5y ago> but eventually you always have to take out the value No. You can just feed it to the function (from a library / stdlib) that needs it, or call .fold() in the end.