[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)