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)
- 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
Zig manages to provide traces with very little overhead:
https://ziglang.org/documentation/master/#Error-Return-Trace...
I am baffled as to why error handling in Go remains so impoverished.
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.
However, doing this kind of ... sucks.
See eg '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'.
> 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.
In Haskell, you can have functions that work generically over option-types, error-able types, lists, functions, tuples, etc.
In Rust, you have to specifically implement functionality for all of these.
But eg your 'map' function in Rust still works for all lists, no matter what item type. In Go before this change, you had to write a different map function for each item type that could be in your list.
In Haskell, the same 'map' function works for lists, functions, error-able types etc.
[1] https://github.com/SeaQL/sea-orm/blob/64c54f8ad603df0c1d9da8...
In Haskell, 'Maybe (Maybe Int)' is a different type from 'Maybe Int'.
That means that when you use eg a hash table that returns some kind null value like 'Nothing' on lookup when a key is not found, you can still stick exactly that kind of null value as a normal value into the table and everything will turn out fine.
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.Emphasis on eventually instead of at every single function call.
No. You can just feed it to the function (from a library / stdlib) that needs it, or call .fold() in the end.
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/
also Result in Rust has been an inspiration
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.
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.So to achieve the same exhaustiveness as a method like
User createUser() throws IOException, BusinessException
you would need both a Result-like construct and union types
Either<User, IOException | BusinessException> createuser()
which few mainstream languages offer. Otherwise you type is unable to express that only IOException and BusinessException are thrown and you don't need to handle a default Throwable case.