1. 31
  1.  

  2. 9

    This strikes me as a step-by-step rediscovery of “Checked Exceptions”, but with an even more awkward encoding.

    1. 10

      Checked exceptions are underrated.

      1. 5

        There’s a lobste.rs discussion from 6 months ago that’s somewhat relevant on that (but with Rust rather than Haskell).

      2. 7

        In PureScript or OCaml, you can use open variant types to do this flawlessly

        Yes!! Polymorphic variants are incredible

        1. 5
          1. 4

            Note that open variants are different from polymorphic variants, albeit both are incredible.

            1. 1

              Oh good to know! Is open variant then the ability to define new exceptions, all of which are part of the exn variant?

              1. 4

                In fact, you can consider exceptions of exn to be a special case of the open, or extensible variant! See https://caml.inria.fr/pub/docs/manual-ocaml/extn.html#sec266 for more details.

                1. 2

                  That’s awesome!

            2. 2

              Another thing I thought was nice to mention: row polymorphism.

              In row polymorphism, variants are open by default. I’ve been toying with writing my own implementation but this one has a nice readme explaining pretty well how it works.

              https://github.com/willtim/Expresso#variants

              The reason given for variant literals being open by default is usually to do error handling and only expose our current problems.

              1. 4

                Polymorphic variants in OCaml work by row polymorphism - this paper introduced the algorithm and resulted in the ocaml implementation. Good luck with the implementation! It’s my favorite type system feature :)

            3. 5

              Errors and exceptional situations should be a carefully considered part of the programming language, not a pattern built on top of the existing data structure mechanisms. Many MLs have an exception construct in addition to sum types, and the clumsiness of error handling that occurs between different layers of an application in both Haskell and Rust is evidence of what happens when you omit exceptions.

              1. 3

                Multi-layer error handling in Rust is pretty much solved by the failure crate. Unfortunately, it took a while to get there, so it’s not part of the standard library. But the standard library’s error type is being altered to operate more like failure, and in a way that failure can easily adapt to take advantage of.

                The failure crate’s approch to multilayer error handling is to provide a macro for quickly and easily defining error sum types (enums in Rust), where each variant corresponds to the unique business-logic-semantics of the error, e.g. InvalidToolchainName. When a lower-level error occurs, e.g. [File]NotFound, it gets chained as the cause of the higher level error. These error chains can be iterated over and each error inspected and downcast to a specific type, or their implemention of Display or Debug can be invoked in order to provide debug information to the end user or developer, respectively.

                This approach combines the ease of use of exceptions, optionally with the static guarantees of checked exceptions (depending on whether or not a default match case is provided), with the performance characteristics of error values. And since the error types are explicit in the function signature, the auto-generated documentation always describes all of the (non-panicking) failure modes.

                1. 3

                  While I agree with smaddox that workable abstractions have been found, I also want to point out that Graydon (creator or Rust) pointed towards errors being an unsolved problem for programming language abstraction. (also, Rust had a couple of error handling mechanics pre-1.0 that were all found to not be usable at scale)

                  https://graydon2.dreamwidth.org/253769.html

                  Rust has a usable implementation given its constraints (it should be working anywhere, from bare metal to high level). For example, exceptions aren’t usable on any system without unwinding, so Rust avoids that.

                  1. 1

                    That write-up was gold in general. My reaction was that language developers try prototyping all that stuff in their languages to see what works. Much as possible anyway.

                2. 2

                  In F# we use result monad with exceptions. I personally encode the typical exceptions I expect via the result type and simply let it crash for everything else, crashing in this case may mean simply logging and reporting the issue. I’ll also use the result type to carry along non-exceptions, things that maybe I want to notify the user about but also aren’t in any way a fault of the code.

                  Something like this.

                  type Problem = Message of string | Exception of System.Exception
                  

                  My functions then return

                  Result<'a, Problem>
                  

                  Where ‘a represents the happy path, and Problem represents any messages we would like to send to the user or exceptions we would like to log. In this way exceptions as an end result represent behaviors that we can’t recover from in any meaningful way and messages represent communication we can do to get the user back to the happy path. I like to recover from an exception as soon as I can and if I can’t i’ll pass it on.

                  The template I provided above is a catch’em all. It doesn’t restrict the errors into groups, but I can use type based matches to focus on just responding to an exception I think I’ll need to respond to such as a SQL Exception.