5 ms·
I think it's fallen out of favor, but I always preferred the "Either" monad style of error handling. I think the cool kids call it "railway oriented programmin
by tombert 3y ago
I think it's fallen out of favor, but I always preferred the "Either" monad style of error handling. I think the cool kids call it "railway oriented programming" but I never liked that term. Regardless, it's not radically different than the Go style, just instead of returning back two things and checking for `nil`, it's wrapped up into an algebraic data type.
I like this better, because you have the option of very easily doing the "Go style" of checking for success or failure if you want, but you can also exploit the fact that it's a monad and a functor and thus you can just use map and/or bind for all the downstream functions, so you don't have to explicitly check if you don't want to.
I feel like this is the best of both world; it still forces you to be explicit (as it's compile-time enforced by the type system), but it gives you nice syntax to make the code cleaner.
- unshavedyak 3y ago> I think it's fallen out of favor, but I always preferred the "Either" monad style of error handling. I think the cool kids call it "railway oriented programming" but I never liked that term. Regardless, it's not radically different than the Go style, just instead of returning back two things and checking for `nil`, it's wrapped up into an algebraic data type. Is that falling out of style? Sounds like Rust's Result type, but i'm unfamiliar with monads/etc. If it is similar, i spent ~5 years in Go and have now had about equal time in Rust - i vastly prefer those algebraic types for errors. Notably because it _can_ be as simple as Go's string concat behavior, but it can also be far more expressive. Yes, Go's type can also be more expressive by adding interfaces/etc, but it's difficult to beat a good enum in my experience. At risk of sounding Rust biased, proper Enums are something that would have made Go more elegant imo. Unlike Generics/etc which are a contentious topic (or used to be, at least), proper enums are just so damn handy and cost so little. My 2c.
- tombert 3y ago> Is that falling out of style? I'm a bit outta the loop in the pure FP world now, because I've been stuck doing Java and C# for the last 5 or so years, but I think that world is trying to move to an algebraic effects-style system. People keep trying to explain to me why that's "better" but I'm not really sold on it, because it just feels like we're reinventing goto, though generally when I have contrary opinions like that it turns out that I'm wrong. I don't really know Rust very well, but I suspect it has a similar way of doing `map` and `bind` to handle downstream stuff, since it has functional roots. The biggest issue I ever had with it in F# is the fact that they don't really have a concept of a monad transformer and therefore you can't easily combine monads, so if you have to mix `async` and the `Choice` monads (more or less F#'s version of Either), you have to explicitly craft your own custom monad. It's really not that bad but it's less sexy than proper monad transformers. Algebraic data types are so utterly useful that I get annoyed that it's not standard in every language.
- tonlage 3y agoI think it's only academic FP that's currently interested in effect systems. most production FP still use the traditional monad error handling. Rust's `Result` type does indeed have a `.map()` function, which works how you'd expect (and `Result`is an iterator, which give syntax for it). Yeah, algebraic data types are very nice, and feel like such a free lunch. I cry every time i have to implement something that should have been a one-line sumtype definition as a class hierarchy.
- rewgs 3y agoThis sounds really nice. Can you think of languages that implement it this way?
- spenczar5 3y agoRust does.
- tombert 3y agoI'm mostly familiar with it in F#, Scala, and Haskell, but I suspect any language with some functional roots has an equivalent. The F# For Fun and Profit blog has a decent post explaining it. That blog doesn't appear to be maintained anymore, but it is archived: https://web.archive.org/web/20180329001511/https://fsharpforfunandprofit.com/posts/recipe-part2/ https://web.archive.org/web/20180329001511/https://fsharpfor... You can find examples in Scala with `for` comprehensions as well.
- arrakeen 3y agojava: Optional<String> result = returnsAnOptional(); if (result.isPresent()) { log.info("result is {}", result.get()); } // and returnsAnOptional().map(r -> log.info("result is {}", r);
- tombert 3y agoOptionals are a bit different though, since they don't provide a way to explicitly handle errors, just wrap the `null` value if it exists and force you to be more explicit about it. I don't know that there's a clean way to do the Either monad in Java without algebraic data types. You could certainly make a wrapper class that maybe has an optional `Left` and `Right` private variable and then make a `map` that handles the unboxing of those, but I think that might be a bit messy.
- arrakeen 3y agomy post was mostly facetious, but it is as you say: possible but probably not something anyone would want to use
- spenczar5 3y agoSadly, I think that is indeed radically different from Go’s design. Go lacks anything like sum types, and proposals to add them to the language have revealed deep issues that have stalled any development. See https://github.com/golang/go/issues/57644 https://github.com/golang/go/issues/57644
- tombert 3y agoI think it's still similar in the sense that the errors are encapsulated explicitly as part of the "return" value. Go gets around the lack of Sum types by requiring you to return two values, but if you wanted to do it the Go style in something like F# you're certainly able to: let res = riskFunc "input" match res with | Choice1Of2 result -> // successfulPath | Choice2Of2 err -> // Error Handling!!! That's more or less what I meant by "not radically different". The implementation is of course extremely different, but I feel like from an end-user perspective it's superficially pretty similar.
- spenczar5 3y agoOh, I see, yes. The F# style is more powerful than the Go style, and can express the Go style if you choose. That’s a pretty good argument that it is superior, but Go might not be able to grow to adopt it
- tombert 3y agoYeah exactly. When there was only one or two risky functions in F#, I typically would just handle it manually with the match function like I described. If there was three or more (which isn't terribly hard if you're dealing with a bunch of streams from Kafka that you're manually merging), I would use the more monadic style. It made the code a lot smaller and more readable, and I really don't feel like it sacrificed anything in terms of readability. Like, at all! There's an overhead associated with boxing and unboxing, so you might be paying a small performance penalty, but since most of what I was doing involved talking to a network a lot, that overhead was immediately dwarfed by IO latency anyway. I don't have any numbers on this, but I suspect that a very large percentage of "risky" functions involve IO of some kind anyway, so the overhead associated with boxing and unboxing is kind of negligible. The lack of algebraic data types and nice mapping constructs is the main reason I almost never end up using Go for any personal projects. I still kind of like the language, I think CSP is a pretty decent concurrency abstraction, but it always feels like it's fighting me.
- tonlage 3y agoone of the alternatives to Go's error handling that OP writes about is the Rust Result type, which is the same as Either. OP seems to think this is the best of the alternatives to the way Go handles things, but that switching to a sum-type approach would be too difficult, given that it isn't compatible in any way with normal Go error checking and a lot of software has already been written in the old style.