1. 13
  1.  

  2. 33

    I’m an Ocaml user and, except for a few rare conditions, I’ve found I much prefer a result type to exceptions. My response will be based on Ocaml which may not be the same as F# so if they don’t apply there then ignore it.

    Some points I disagree with the author on:

    AN ISSUE OF RUNTIME

    I didn’t really understand the example here. How is the author accessing an optional value? In Ocaml we have to use an accessor that would throw an exception if the value is not present or pattern match the value out. This doesn’t seem to have anything to do with exceptions or results, just an invalid usage of an option.

    AN AWKWARD RECONCILIATION

    This is the case in Ocaml as well, which is why many libraries try to make exceptions never escape the API boundary. But writing combinators for this are really quite easy. A function like (unit -> 'a) -> ('a, exn) result is available in all the various standard libraries for Ocaml.

    BOILERPLATE

    The author should be using the standard applicative or monadic infix combinators. Maybe F# doesn’t allow that. In Ocaml the example would look like:

    let combine x y z =
        pure (fun x y z -> (x, y, z)) <*> x <*> y <*> z
    
    WHERE’S MY STACKTRACE?

    This is the one I disagree with quite a bit. If I am using exceptions then yes, I want stacktraces, because it’s a nearly unbounded GOTO. But the value result types give me is that I know, using the types, what errors a function can have and I have to handle it. This makes stacktraces much less valuable and the win of knowing what errors are possible and being forced to handle them. I’d much rather have this than stacktraces.

    THE PROBLEM WITH IO

    The problem here doesn’t have anything to do with exceptions, it’s that the return type should be a result where the Error case is a variant of the various ways it can fail. Ocaml makes this much much easier because it has polymorphic variants.

    STRINGLY-TYPED ERROR HANDLING

    Yeah, use a variant not a string.

    INTEROP ISSUES

    This can indeed be a problem. It’s also a problem with exceptions, though.

    1. 9

      100% agreed. Debugging from a stack trace is far more complicated than having good error handling through compiler enforced types.

      1. 3

        Ditto. This is the case I’ve found in any FP language that I worked at, it takes more time to work with the stack trace, and recover anything valuable from it, instead of utilizing the compiler and the type enforcing at compile time.

      2. 2

        WHERE’S MY STACKTRACE?

        This is the one I disagree with quite a bit. If I am using exceptions then yes, I want stacktraces, because it’s a nearly unbounded GOTO. But the value result types give me is that I know, using the types, what errors a function can have and I have to handle it. This makes stacktraces much less valuable and the win of knowing what errors are possible and being forced to handle them. I’d much rather have this than stacktraces.

        This is a case where you can eat your cake and have it too. Java has checked exceptions which the compiler enforces are handled. When call a function that can throw a checked exception, the calling a function either has to handle the exception in a try block, or include in its signature that it can throw an exception of the specified type.

        You can also do the opposite and add the stack trace to the result type. Most languages provide some way to obtain a stack trace at runtime, so all you need to do is attach the stack trace to the error when it is instantiated.

        1. 4

          Checked exceptions in Java are a nice experiment but a rather colossal failure, unfortunately. Since the compiler cannot infer checked exceptions you have to retype them all out at each level and it becomes unwieldy. The situation is even worse with lambda’s where one has to turn a checked exception into an unchecked one.

          1. 3

            Is it simply type inference on function declarations that you see as the difference here? I am curious because as a Java programmer by day, I don’t see a ton of difference between “-> Result<FooVal, BarException>” and “FooVal someFunc() throws BarException { … }”.

            Granted the implementation is quite different (unwinding the stack and all that), but is it simply ergonomics that makes the latter a “colossal failure” in your mind?

            1. 3

              No, the difference is that results are just types and values. From that you get all the great stuff that comes with types and values. For example:

              • Type inference. I only specify the types of my functions at API boundary points.
              • Aliasing types. If I have a bunch of functions that return the same error I can just do type err = ..... rather than type all of the errors out each time.
              • They work with lambdas!
              • They work with parametric polymorphism. I can write a function like 'a list -> ('a -> ('b, 'c) result) -> ('b list, 'c) result.
              • And, probably most importantly, it does not add a new concept to the language.

              That checked exceptions do not compose with lambdas in Java basically tells me they are dead. All the Java code I’m seeing these days makes heavy use of lambdas.

              1. 2

                Gotcha, thanks for the reply. I don’t disagree strongly, but I feel like what you are arguing for is Java, minus checked exceptions, plus more pervasive type inference, plus type aliases, plus several other changes. Which, that’d be pretty cool, but I think at this point we’re discussing sweeping language changes as opposed to the merits of checked exceptions strictly.

                For example, simply replacing checked exceptions in modern Java with use of a Result would (at least as far as I can imagine) still result in a lot of verbosity. You’d just be typing “Result<Foo, Bar>” a lot as opposed to typing “throws Bar” a lot.

                Not to be overly argumentative or anything. But “colossal failure” seems a little strong to me! :)

      3. 3

        I always did error returns to make things explicit & easy to analyze. One can sugar coat them with syntax or functions to eliminate boilerplate. Here’s three links on the topic I found useful:

        https://www.joelonsoftware.com/2003/10/13/13/

        http://nedbatchelder.com/text/exceptions-vs-status.html

        https://news.ycombinator.com/item?id=9828068

        The third one was responding to the concept that error code returns all have to be ugly piles of boilerplate making code hard to read. That comment is an example of avoiding that in C code without switching to exceptions.