[HN Gopher] Fault Report - and alternative to Result in F#
___________________________________________________________________
Fault Report - and alternative to Result in F#
Author : fire_lake
Score : 56 points
Date : 2024-12-18 12:04 UTC (4 days ago)
(HTM) web link (paul.blasuc.ci)
(TXT) w3m dump (paul.blasuc.ci)
| omcnoe wrote:
| As a daily user of F# I'm not sold on this suggested alternative.
| It seems to be trying to make it as easy as possible to make the
| Result type behave like exception handling, but I think there are
| some key downsides to the approach.
|
| First, Result is often used in place of Choice only to
| communicate the intent that the 2nd case is an error case. Making
| it more work to use Result (by requiring the types used for the
| Error to implement some special interface) means programmers will
| simply use Choice instead.
|
| But the bigger issue is that this suggested change would remove
| the ability to ensure that cases of an error type are
| exhaustively matched. They introduce the FailAs active pattern at
| the very end of the blog to deal with logic that wants to inspect
| the underlying error cases, but active patterns break the
| compilers ability to check if a match is exhaustive. This is a
| really useful property for program correctness, I can ensure that
| a caller of my function will receive a compiler error when a new
| error case is introduced that wasn't previously handled.
|
| I wonder if a better design for the error type could be some kind
| of error type set that could automatically collect possible error
| cases up the call stack, so that when confronted with an IFault
| you at least know what possible cases it could have. With some
| type inference help it might not even be that painful to deal
| with, the first example could automatically infer a type like
| Errors<AtlasError, ForExError>.
|
| As touched on briefly in the article, the elephant in the room
| here is that changing the Result type doesn't do anything to
| unify the two distinct worlds of exceptions vs error values that
| currently exist in F#. Dealing with obscure C# exception flow
| from F# has been the biggest source of subtle, hard to track down
| bugs in my production code.
| needlesslygrim wrote:
| I agree, this doesn't seem to be much better than adding
| `throws Throwable` to all your methods that can fail in Java.
| The reason sum types are so useful for error handling is that
| all possible cases can be exhaustively checked against at
| compile time.
| nozzlegear wrote:
| I'm also a daily user of F#, and like you I'm not totally sold
| on this alternative. However, at first glance (without using it
| in a real life scenario) I like it more than the Result type we
| have. One of the apps I have in production right now leans
| heavily on using Results, and the biggest pain point -- besides
| handling exceptions from C# world -- is the mapping of the
| different Result error types. It can get tedious, especially
| when you're three Results deep in a "railway" and each
| operation needs its own custom mapping because they use
| different Result types.
|
| Before reading this article, I'd decided that if I had to do it
| again I'd just throw exceptions, but now I'm wondering whether
| or not I'd play around with enforcing an IFault interface.
| EdwardDiego wrote:
| It's been a very long age since I wrote any F#, really enjoyed it
| at the time and it was for fun stuff, I wasn't trying to deal
| with errors, just things like process a prefix trie in a cool
| way, but does Result<'T, 'TError>
|
| really have no upper type bounds on TError?
|
| I'm assuming that's the case from the fact that he's arguing for
| a basic interface (Does F# support structural typing, or do you
| need to say that AtlasError implements IError or whatevs?), and
| I'm really surprised there isn't one.
|
| Huh, would love to know why leaving it unbounded was decided
| upon. Might be I don't quite grok the culture of F#.
|
| This reminds me of a very common and awful Python pattern, where,
| as there's no `message` attribute on the base error/exception
| classes, despite every nearly every exception or error having
| one, this is the normal pattern to access it:
| message = ex.args[0]
|
| It gets really fun when you get an IndexError because one
| exception had no message provided when instantiated, so then you
| find code like: message = ex.args[0] if ex.args
| else None
|
| Yech.
| Guvante wrote:
| Rust leaves it unrestricted. As does Haskell (although I am
| lying a little most just choose to use Either for Result)
|
| Rust actually gets some use out of it, for instance returning
| an alternative value instead of handling it as a pure failure.
| EdwardDiego wrote:
| Oh true, I am really interested in those choices too.
| remexre wrote:
| The case that comes most to mind for that in Rust is
| https://doc.rust-
| lang.org/std/primitive.slice.html#method.bi...
| EdwardDiego wrote:
| Thank you!
| smoothdeveloper wrote:
| /!\ this is just my perspective /!\
|
| > would love to know why leaving it unbounded was decided upon.
| Might be I don't quite grok the culture of F#.
|
| You may look at more context in the discussions pertaining to
| the RFC: https://github.com/fsharp/fslang-
| design/blob/main/FSharp-4.1...
|
| I think those are the main factors:
|
| * "Keep It Simple Stupid" principle
|
| * community pressure so that F# libraries could standardise on
| better things that Choice1Of2 = Ok and Choice2Of2 = Error, or
| using a bare tuple (which is very not clean for APIs)
|
| * F# ought to remain flexible in terms of not pretending of
| totally abstracting away the fact that most of the dotnet base
| class library or the overall dotnet ecosystem, uses exceptions,
| it is not like Result type was created to abstract runtime
| exceptions away.
|
| For all other needs, one is just one wrapper / or abstraction
| (for library that don't "meet our standards" in terms of
| exception safety) away, along with a bit of adhoc tooling
| around leveraging FSharp.Compiler.Service for sake of adding
| static analysis, for architecture astronauts, dealing with
| behemoth or safety critical codebases, requiring even more
| screws turned, but even there, F# (and so many other languages)
| may not meet the bar if those are the critical properties for a
| project involving code.
|
| Overall, F# is just pragmatic for the 99.99%, and not trying
| too hard beside the steady core language idioms, and how the
| organic community wants it over time, to satisfy hypothetical
| and intellectual pursuits of the highest abstraction and
| safety.
|
| The culture of F# is mostly about throwing productivity parties
| and being done with it (while not suffering so many of the
| wrong choices done by C, C++, Java and it's famed sibbling,
| C#), rather than the higher conceptual type theory perfection
| that (sometimes ?) lead to code that is not less kludgy, or
| easier to maintain, in the grand scheme, for the masses of
| developers, that are not yet expert in Haskell, Scala, etc.
|
| It is probably not too dissimilar to OCaml culture, while
| embracing some aspects that are familiar to more people, due to
| relationship with dotnet ecosystem.
| EdwardDiego wrote:
| Thank you, I'll read your link, but really appreciate your
| perspective. :)
| neonsunset wrote:
| C# is not a Java sibling :(
|
| (There are languages more similar to C# than Java and vice
| versa, and of course the underlying type system differences
| between JVM and .NET also add to this rift, C# was intended
| as a successor to C++ just as much as it was to Java)
| psd1 wrote:
| They are not identical twins nor byte-compatible. Collect
| your prize on the way out.
| akdor1154 wrote:
| I'm pretty sure this is isomorphic to Go's err, right?
|
| Report ~ res, err (well this bit not strictly, but in practice
| close enough)
|
| IFault ~ Err (interface over a string repr, and a way to wrap a
| cause Err with err.Unwrap)
|
| In my experience if you land on a design that looks like
| something either Go or F# does then you're probably on the right
| track. :)
| neonsunset wrote:
| Go lets you ignore the errors and does not have tuples - only
| multi-variable returns so you can't _not_ deconstruct the
| return value (and can 't .map it as a result too).
|
| I don't think it's fair to place the two next to each other.
|
| F# also does exception handling (where it makes sense, though
| TryParse is more efficient): let values =
| ["abcd"; "1234"] let number = try int values[0] with _ ->
| 1337
| gf000 wrote:
| > In my experience if you land on a design that looks like
| something either Go or F# does then you're probably on the
| right track
|
| Well, let's just say that our experiences are vastly different
| then, but I had to do a double take when I saw Go mentioned as
| a positive from a PL design perspective..
|
| And no, Go can literally return both a value and an error at
| the same time, it has possibly the worst error handling out of
| any "modern" language.
| daxfohl wrote:
| Main thing I dislike about errors as ADTs is it forces
| indentation in both cases. I miss go's "iff err..." otherwise
| continue as normal. Yes monads can allow that, but they come with
| their own problems.
| Smaug123 wrote:
| Au contraire! You can unindent the final clause of an `if` or
| `match` block. Fantomas even supports it
| (`fsharp_experimental_keep_indent_in_branch = true`).
| daxfohl wrote:
| Oh, good to hear! My knowledge is about 7 years out of date
| at this point. I know typescript and mypy are pretty good at
| inferring which options of an ADT are impossible in various
| contexts and letting you do stuff like this, so great to hear
| F# has upped its game here too.
___________________________________________________________________
(page generated 2024-12-22 23:01 UTC)