Remix.run Logo
ameliaquining 4 days ago

The difference is that there's only one exception type and it can't carry payloads. This turns out not to be very different from an option type like in Rust or Swift, just with a bit of syntactic sugar around it.

spopejoy 3 days ago | parent | next [-]

Hmm ... How do you implement the common pattern of a top-level exception catcher (e.g. for a long-running thread) with option types in pony/rust/swift? In Haskell this is ergonomic thanks to Functor and friends, but how do langs other than Haskell propagate this behavior up the call stack?

ameliaquining 3 days ago | parent [-]

So for baseline context, we can consider two kinds of errors; there's no standard terminology, but I'll call them "exceptional results" (caused by problems outside your process's control, like filesystem or network failures) and "runtime errors" (caused by bugs in your code, like trying to dereference a null pointer or index past the end of an array).

Pony: Doesn't distinguish between exceptional results and runtime errors; either a function is guaranteed not to fail, or it isn't. A function that isn't is called "partial", and can only be called either from another partial function or from a try block that specifies how to handle errors. So anything that can fail in any way, including indexing into an array, has to be partial; there's no way to tell the compiler "please don't force my caller to handle the possibility of a bounds check failure, I'm sure that won't happen, and if it does then everything is terminally messed up and we should just crash". Likewise with a value of a union type being None when it shouldn't be, or otherwise being the wrong type. (They tried having integer division be partial, but apparently that was a bridge too far—so instead, any integer divided by zero is zero.) The top-level error-handling pattern would need to be implemented by making basically every non-leaf function in your codebase partial. Also, these errors can't have payloads or anything, so the top-level handler can't make use of any diagnostic information (if you want that then you need to instead return a custom union type and forgo the partial-function syntactic sugar). I haven't actually used Pony in anger but this is definitely the feature of the language that impresses me least; it's ostensibly in the name of correctness, by prohibiting programs from crashing, but you can't actually prevent the root causes of crashes without dependent types, so instead they've made this mess that makes effective error handling impossible.

Rust: Exceptional results are represented with the Result type, instances of which are just values returned from a function like any other. There's syntactic sugar (the ? operator) to propagate an error up a stack frame. A Result has a payload whose type must be specified; ergo, every function's API must effectively declare not only whether it can return exceptional results, but also what kinds. In a library, you'd typically design a structured error type to represent the possible exceptional results that your API can return, but in a binary, you may not care about that level of detail in each function's API, and instead prefer to return a catchall "some exceptional failure, idk what" type. Out of the box the language has Box<dyn Error> for this, but it's more common to use a third-party crate like anyhow or eyre that makes this pattern more ergonomic. In an application that handles errors this way, most likely every function that does I/O returns Result<T, Box<dyn Error>> (or the anyhow or eyre equivalent), while functions that just do pure computation on trusted data return non-Result types. Runtime errors use a different system, panics, which are not part of a function's API (so any function can panic); by default, panicking unwinds the call stack until it reaches a catch_unwind call or the bottom frame of the current thread's stack (allowing a top-level handler to use catch_unwind or Thread::join to catch and handle runtime errors), but when building an executable binary you can instead configure it to skip all that and immediately crash the process when a panic occurs (there are pros and cons to doing this). For this reason, library code can't rely on catch_unwind working, so it's important for libraries not to try to use panics to convey recoverable errors. A Result can be turned into a panic with the unwrap method, if you're sure that the relevant exceptional result can't occur or if you're just being lazy/writing prototype throwaway code and don't want to bother getting the function signatures right.

Swift: Exceptional results work like checked exceptions in Java, but without all the problems that make the latter not work very well in practice. Each function signature can either never throw anything (the default), throw any Error (with an arbitrary payload whose type is unknown at compile time, analogous to Rust's Box<dyn Error>), or throw a specific Error subtype with a specified payload (this last one is uncommon). When calling a throwing function, you must either handle the error or (if you throw a compatible type) propagate it up to your own caller; there are various syntax-sugar niceties to handle this. There's a Result type like in Rust but it's not commonly used as a return value, it's mostly only needed if you're storing errors in data structures or similar. So a top-level error handler is done by having all functions that do I/O be throwing functions, like in Rust but with nicer syntax. Runtime errors are not declared in function signatures and always immediately crash the process; unlike in Rust, there is no way to have a top-level handler for them. Like in Rust, you can turn an exceptional result into a runtime error (Swift's syntax for this is the try! operator).

__red__ 2 days ago | parent [-]

Pony is a strongly typed language. If you want your functions to return an Optional Type, define an Optional Type.

For example, the OpenFile API returns you either a valid pony File object, or why it failed (FileEOF, FileBadFileNumber, FileExists, FilePermissionDenied, etc…).

What partial functions do is ensure that all of the error cases are actively addressed by the programmer, so you don't get panics or SEGVs.

ameliaquining 19 hours ago | parent [-]

The problem is, what exactly do you do when your program hits a precondition or invariant violation?

There are basically only two possibilities: abort the process, or jump to a top-level handler that logs the error and returns a 500 response (or whatever the equivalent is, for a long-running program that's not a web server). In neither case is there any interesting decision to be made about how the direct caller should handle the error. In return, whenever you subscript an array or otherwise do anything that has preconditions or invariants, you have to add a question mark to every function that can transitively call it and every call site of same, throughout the codebase. This task requires no intelligence—it can be done algorithmically without error—so why require it?

If you could actually prevent precondition and invariant violations at compile time, that would be quite a different proposition, but Pony can't actually do that (because it requires dependent types) and the intermediate solution they've hit on appears to be the worst of all worlds.

Also, it seems perverse to have a syntax for calling fallible functions and propagating failures up the call stack, but then not allow it to be used for exceptional results, which are the kind of failure that's actually worth propagating up the call stack.

__red__ 16 hours ago | parent [-]

You can do this:

  try
    somearray(outofboundindex)?
    // stuff
  else
    errorlog.write("some message")
    error
  end
Sure you can make it propogate all the way up if you really want, but ugh… what a terrible idea for instrumentation.

Pony doesn't force you to deal with errors a specific way. Pony forces you to make a choice so you can't just ignore it.

ameliaquining 14 hours ago | parent [-]

That code is only allowed in a partial context, right? So if you don't propagate it all the way up the call stack, what do you do instead?

pjmlp 4 days ago | parent | prev [-]

In Rust and Swift they can have payloads and variants, which in Rust's case due to lack of ergonomics, there are plenty of macro crates to work around this.

ameliaquining 4 days ago | parent [-]

Yeah, to be clear, it's similar to an option type rather than a result type.