omcnoe 11 hours ago

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 10 hours ago

    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.

EdwardDiego 11 hours ago

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.
  • smoothdeveloper 9 hours ago

    /!\ 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 8 hours ago

      Thank you, I'll read your link, but really appreciate your perspective. :)

    • neonsunset 7 hours ago

      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 7 hours ago

        They are not identical twins nor byte-compatible. Collect your prize on the way out.

  • Guvante 11 hours ago

    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.

akdor1154 11 hours ago

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. :)

  • gf000 9 hours ago

    > 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.

  • neonsunset 10 hours ago

    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