Rust: The Error Handling Project Group
blog.rust-lang.org
blog.rust-lang.org
* Ignoring the error
* Terminating the program
* Using a fallback value
* Bubble up the error
* Bubble up multiple errors
* Match boxed errors
* Libraries vs Applications
* Create custom errors
* Bubble up custom errors
* Match custom errors
As for implementation using unwinding vs a sum type, they are semantically equivalent, but using a sum type like Rust does allows you to use exceptions even when the error case is common without the steep performance degradation that happens with unwinding, at the expense of a very slight slowdown in the no-errors case.
Rust also supports unwinding/aborting exceptions as "panics". These are implicit because all accessible mutable memory is destroyed by a panic, since you must not keep references across catch_panics, so there is no need to rollback (except for unsafe code, in which case you need to assume that all function calls can panic).
I haven't touched Java in 10 years, but didn't it have explicit propagation with `throws` statement for this exact purpose?
If the exception is not a RuntimeException and not declared in the throws clause, then the compiler will error out, but in other cases the code will compile.
In Rust, you need to use the "?" operator when you call an exception-throwing function to propagate the exception, and if you don't you will get a Result, and a warning if you don't use it (i.e. not using "?" in Rust is like using Try in Scala).
If Rust code has no "?" or "return", then no exceptions will be thrown and the code will fully execute, except for aborts and panics which are guaranteed to make all visible memory inaccessible, so you will never see a partial modification of an object due to an unexpected exception being thrown.
Similarly, all unchecked errors and exceptions mean that you never know. So you have to treat critical sections of code as areas where resources need to be cleaned up by using try and finally blocks.
Citation needed on this one. In almost two decades I never once came across code that straight away required such rollbacks.
Consider a simple case where I am trying to receive data over a socket and write it to a file. I need to 1) open the socket, and 2) open the file. Whichever order I do these in, if the second one fails I need to be sure I clean up the former.
Use a protocol that can cope with a disconnect well. Write to a temporary file which either you then rename, or the OS discards if there was a failure. Trying to “clean up” manually can break things further since you don’t and can’t account for all possible reasons for a failure.
A simple example: your hard drive is failing, and performing any kinds of cleanups on it will get your program stuck even further. Also, does it really matter too much for your stated purpose if you get an ENOSPC, an EPERM, or an EIO?
Sure, in a sufficiently crazy situation, close() might not actually close the file descriptor - maybe I've somehow trampled on the memory associated with the function (having worked around any kind of W^X), or maybe the operating system is just seriously fucked - but it seems crazy to expect that on the basis of failing to open a file. You should still close the socket.
That cleanup phase is generally some kind of rollback, usually in the form of discarding whatever partially constructed datastructures (sometimes in the form of a savepoint and reverting, but thats rarer ime).
The other type of catch handling is to just force everything into some sane default state, to avoid having to care what specifically borked
Beyond that, there are other reasons, but in my mind, that is the fundamental issue. Everything else kind of rests on top of that.
I do believe there's been some discussions in C++ land about the relative performance of these approaches, though I can't find good references right this second.
Someday we should push multiple return points, one for each enum variant of the return type, and then we avoid the branches too. (This approach is described in https://www.ccs.neu.edu/home/shivers/papers/mrlc-jfp.pdf ).
For exception-like Result enums where you want to predict the Ok variant every time that's probably fine (as long as your CPU is new enough that taking the error path won't cause it to mispredict every return afterwards), but it may or may not be a win in the general case of enum returns.
The language also offers some more language support for declaring that some things will never use exceptions, via noexcept. This matters a bit more because, since the language itself has exceptions as a feature, some things assume that they exist in order to work properly, in my understanding.
That is true and in embedded projects it is common that exceptions are forbidden, which shocks many young developers who are not used to write C++ without exceptions;-) In C++ it's nice when you have exceptions but without them you are very much on you're own, almost as you were writing C. Additionally every library does error handling slightly differently and you quickly end up in a world of pain.
The really nice thing about Rust, in that regard, is that there is no need for different error handling systems. No matter if you are writing very low level code (e.g. embedded systems) or high level (e.g. web apps) code the basic error handling primitives are the same. Since we have the question mark operator the ergonomics are not too different from exceptions in other languages. What is not so clear at the moment is, what the best practices are on how to combine these primitives. As far as I understand this is one of the major areas the Error Handling Group is to address.
Implicit (unlike Java's throws declarations) exceptions fit well into a particular error-handling strategy of handling some expected errors, but by default, crashing the whole app on any unexpected error. It works pretty well for some applications, especially for web apps that operate on request-response model and have no local state: nothing could be easier than simply crash the VM, send the error report and restart the app back again.
However, this strategy doesn't fit all possible situations. Example, you may need to know when something can throw so you would be able to roll back to a valid state.
Often, what you want from a call to some third-party code is to explicitly define all possible error conditions that it can return. Java's `throws` handles that in some regard, but `Option` or 'Maybe` or `Result` monad (which has a lot of different names in different languages) is easier to reason about, and since it is usually implemented in languages that have pattern matching syntax, it also looks read to easier, more readable code, at least in my opinion.
Now, I have to admit that ? and ! syntax sugar in Rust look very exception-like, and when I wrote web apps with it, I found myself having absolutely the same mindset as I had with exceptions. With one important difference: if I would be so inclined, I could have very easily gone through all the codebase and made all error handling much more strict and explicit. Regardless of how giant the codebase would become, the type checker would have my back in this process, and if I didn't want to leave some unknown error condition left behind in the depths of the call stack, it wouldn't be.
2. it also makes it difficult to handle nested errors (eg the catch() itself throws)
3. unchecked exceptions (and implicit propagation) means any function can throw something, at any time -- it's not defined in the API contract -- so if you really wanted to be safe and general, every function needs have exception checking. This is the same problem that null has
4. It also means it can change "under your feet" -- a library can add new exceptions to functions with no indication; this is also possible in rust, but its requires much more finagling by the author.
5. Checked exceptions like in Java are basically the same as rust's error-as-enum, but feature the other points.
6. At least in Java, not all exceptions are checked, but of course that doesn't need to be true.
7. Through developer ergonomics, Exceptions leads the natural strategy of try it, and rollback on failure. Rust Result<> leads the natural strategy of validate, then continue. The latter, IMO, is significantly easier/general/more sane.
8. Through developer ergonomics (avoiding a try-catch every statement), multiple statements are naturally rolled into a single try-catch -- undone when this turns out not to be the case. In the worst case, if you try-catch every statement in a function, its completely unreadable. It's also difficult to know which function throws which error, if you have them rolled up in a single try-catch, with multiple catches.
9. Exception's main job is to put errors in the background; Rust has the philosophy of putting errors in the foreground
10. The main benefit they have over return codes is that return codes are utterly arbitrary, and even less defined in the API contract, or the language itself, than exceptions. They lose out on the other points -- namely that they're significantly more complex. Result<> grants the natural ergonomics of return codes, with a higher level of definition than exceptions.
11. Try-And-Rollback is a generally valid strategy for single-threaded code; less so for parallel code (eg fork-and-join). Result<> fits that model more cleanly.
12. My personally most important point: exceptions are ugly and anyone who likes them is ugly too
That's whatever I could think of off the top of my head.
I hate exceptions with a passion, especially if you throw in async exceptions.
All the new 500s we get at work are exceptions that we're not catching somehow because the developer who wrote the code didn't expect that scenario.
Unchecked exceptions are well, unchecked. Allowing unchecked things would completely compromise the Rust safety for no gain at all.
But well, I'm not sure what you are complaining about. The examples where the error is not treated at the point it happens look quite similar to what a try-catch would look. What code do you think error handling gets in the way of the "normal" path?
I think this is very true of Java checked exceptions. I am not sure this holds up if we actually offered a more sophisticated language for talking about exceptions.
Some of my recent comments on the topic: https://news.ycombinator.com/item?id=24454663 https://news.ycombinator.com/item?id=24448865
Ultimately, both are equally expressive and it's reasonable to personally prefer either. The "Result" type approach maps better to the concept of a program as a series of inputs and outputs (functions).
To be fair, Rust effectively does have exceptions, in the form of panics. They unwind the stack, can have different types (although &str/String are the most common/supported), can be caught and rethrown if not configured to abort the entire process instead.
They are used for exceptional, fatalish errors (including admittedly less exceptional bugs from unconsidered edge cases), and not regular "normal" / "expected" day-to-day error handling.
https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
https://doc.rust-lang.org/std/panic/fn.resume_unwind.html
> I'm pretty happy with them in other languages, as they allow me to focus more on the "normal" path in my code.
This is the boon and curse of exceptions.
Consistently writing exception-safe code when you're focused more on the normal path can actually be quite hard, as evidenced by all the code I've debugged written by coworkers who've failed to do so. Reviewing said code to attempt to enforce exception safety is similarly hard. Rust's `?`s, by contrast, make early-outs relatively easy to spot and reason about via their syntax.
Aside from leaked memory, double frees, and other bugs and crashes caused by invalid object states from failing to consider the case where execution is interrupted mid-operation by an exception, there's also all the undefined behavior from exceptions unwinding across C ABI boundaries for callbacks and other purpouses. This is suprisingly common in gamedev.
The larger and more widely used the codebase, the more frequent and normal the "abnormal" path becomes, and the more serious the consequences of ignoring it. Nobody cares if a quick utility script or command line tool occasionally crashes if you can just rerun it, or have it quickly patched, and don't suffer data loss. Focusing on the normal path is probably the right choice there!
But at the other extreme, if you're about to release a game, a crash affecting 1% of players is still pissing off thousands of people, and not all handhelds have internet connectivity for post-release patching. This results in weeks-long QA processes to try and catch all the nasty stuff, because delaying the release and throwing a wrench into a million dollar marketing plan might be less expensive than shipping shit broken by something as mundane as an unexpected exception. Focusing on all the edge cases is often the right choice there.
And when focusing on all the edge cases, explicit return based error handling tends to be much easier to read and reason about than exception based code, IME - even in Rust where a lot of heavy lifting might be hidden behind the occasional stray "?" - letting me get back to focusing on the "normal" path of my next task quicker - and with more confidence that I considered the appropriate edge cases of the previous task exhaustively and successfully.
Of course there are a few rough edges, but nothing major. I also don't mind having to wrap the successful result of a fn in Ok(...).
I hope "do a few minor things, add better documentation, but leave the language itself as it is" is also one of the possible outcomes...
I mean, failure literally says that it is experimental, that's the first thing it says. And much of its explorations were then integrated into the standard Error trait.
anyhow makes it much clearer that it targets convenience for applications: it aims to be a better `Box<dyn Error>`, not to help with structured error handling but to propagate errors out of the way.
I've been using failure and would like to know if I should switch.
Also is it even possible to standardize error handling best practices across entirely different software components like an OS kernel, a web service and a CLI tool?
OS kernel should not panic? Service absolutely should panic and restart to maintain availability and consistency if something unexpected happens (see Erlang) and a CLI tool has a human in the loop, sometimes.
There is, and this is exactly what has been happening. Early crates showed some of the weaknesses in the Error trait, and then they were addressed. Failure was created, showed more holes, and they were addressed. Now anyhow/thiserr/other stuff have happened, and their improvements may or may not make it upstream.
> Also is it even possible to standardize error handling best practices across entirely different software components like an OS kernel, a web service and a CLI tool?
Some of the details differ, but it feels like we're at that point to me.
> OS kernel should not panic? Service absolutely should panic ...
Both of these are specific implementation choices, and some people will choose different options for each of them. It just really depends. But with Result (and to some extent, the Error trait), there are means to share the bits that make sense. The core of it is broadly applicable even if some of the details change.
I agree that "what to do about errors" differs a lot in those domains (and even more in some others, like embedded). That's not a reason not to standardize - it's something the standard needs to take into account.
Ideally, if I am writing a bit of code that could be in more than one of those domains, there is something standard I can do about errors that will provide sufficient flexibility to my users that I can act appropriately to the domain I actually find myself in. Without too much complexity.
To me, there is a large difference between them.
Result is for computations that can fail, such as opening a file or reading from a socket. Computations that fail should be handled by the caller (e.g. report a nice error which file could not be opened or attempting to open a new socket and retry the communication).
panic, in particular through assertions, are for catching programming errors, such as violation of invariants. These are cases where the program should just crash and burn, because it would be in an invalid state otherwise. E.g. splitting a Vec at an invalid index is a programming error. If a program attempts such a split, it is already in an invalid state, so it should crash and the bug should be fixed.
What I usually do in other languages is use exceptions as assertions, when it is just a simple one-off app, but then go back and catch the exceptions when possible to make it more user friendly and robust.
The Rust equivalent is letting Result::Err bubble up the call chain and have a
fn main() -> Result<(), Error>
or even nicer: fn main() -> anyhow::Result<()>It's possible to catch panics during unwinding[0], but that will not work in every case: `panic='abort'` is a completely valid configuration for a Rust compilation.
> What I usually do in other languages is use exceptions as assertions
Rust assertion macros will panic. `unwrap` methods are also, functionally, assertions. And they panic.
> when it is just a simple one-off app, but then go back and catch the exceptions when possible to make it more user friendly and robust. It seems that exceptions have been omitted on purpose, but is there another way to express this idiom in Rust?
That is very much not idiomatic rust[1] and I would recommend against it, but it is possible for applications as you'd hopefully be controlling the compilation configuration.
For libraries however it's a good way to shoot everyone in the foot: you simply can not assume that users of your library will `panic='unwind'`, so it is a very dangerous thing to do.
[0] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
[1] https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-pa...
If you don’t want to handle Results, just ? all the way up and out of main. Then come back and deal with them as necessary when you’re ready.
It’s far nicer than dealing with exceptions.
So I don't think language changes are going to come out of it.
Personally I follow with the trend and feel strongly that the Result-type approach is superior over (unchecked) exceptions in most cases, since: (1) they are much easier to reason about for me as a programmer reading and writing code, as it (2) produces code with easier execution flow and (3) uses the type system to specifically+exhaustively communicate the possible outcomes.
I say "in most cases", as I believe there are (probably) some behaviours that are easier to express using exceptions, but in practice I rarely run into such cases. And when I run into them, I might simply not be able to come up with the right abstraction using Result-types.
I find the Rust/Kotlin trend of only allowing one exception (the panic), as to dictate the language users to use other means to communicate/express multiple outcomes of a call. Both languages --- by having sum types + pattern matching switch statements + exhaustivity checks on switches over sum types --- also provide means to express/handle multiple outcomes of a call, thus rendering the prevalent use of exceptions mostly irrelevant.
The hierarchy of exceptions that you mention are to me something that goes against the nature of typing discipline: they subvert the typing system (in case of unchecked) or add a additional typing component (in case of checked) to express/handle multiple outcomes of a call.
I would hate to have to reason about the 250 possible errors coming from every single library underneath my app, especially when the immense majority of those are better left to be solved by the user (e.g. file cannot be opened because the usb stick was removed, audio device unavailable because you are using a pro audio driver that locks to a single app, the GPU cannot be opened because I did a driver update and must reboot, etc etc).
Monadic error handling is fine when you have a finite, predetermined set of "things that can go wrong" - e.g. you're writing a small input parser for a text field, either it parses or it does not. But imho this is a microscopic amount of cases compared to all the "random stuff that can go wrong" in the average GUI app. And as a GUI app user, I really really hate when the app tries to do what's best for me - autoreconnect, write to another file, change the settings automatically, whatever. Just gimme an error popup and let me decide.
> produces code with easier execution flow
In C++ using RAII objects for state makes that concern pretty much disappear imho. Save your state & rollback in a tiny type's ctor/dtor and you're good to go, whatever you do, things can throw and what you were doing will be rolled back. Not as good as hardware transactional memory, sure, but apparently not a lot of CPU engineers are able to make that one work yet...
> (3) uses the type system to specifically+exhaustively communicate the possible outcomes.
back to 1, this is what I really do not want. Just write the happy / known-to-the-programmer path and let what you don't know about bubble up to main() or the top of your event loop.
That's is why I think exception are bad: the programmer usually ends up not thinking about the errors that can occur (and they often cannot even do it if they wanted because exceptions can come from anywhere) and just propagates to the user, who often doesn't even have access to the documentation of the underlying library, and must perform Google search on some cryptic error message, just to discover that the program is failing because some log formatting library he doesn't care about cannot have access to the configuration file it expected. This is a terrible user eperience.
And yes, checking 250 different kinds of error can be tedious (but it should never be so high anyway), but at least with sum types it's doable because the type system is here to help you cover every case. And then you can manually check which error is a reasonable one to return to your user and which is a recoverable one that should need no external intervention.
If the USB drive has been pulled out too quickly, just tell it to the user instead of returning a “aborted: did not have the rights to write to the file” exception.
you know what is even worse ? the programmer "handling" the error and just displaying a "we're sorry, an error has occured" message to the user who now has exactly zero way to know what is going wrong. Or as seems to be common in Rust, Go, and other exception-free systems, just calling panic! ... yay, "error handled".
It is very irrealistic and presumptuous to assume that your program has enough domain knowledge about its users's systems to handle every single error.
This occurs a lot with language with exceptions too: you need to “handle” the exception before it reaches the root, otherwise it crashes the whole software (which is basically the same behavior than a panic in Rust or Go), so people usually add this kind of catch-all exception handling in Java or any language with exceptions. Have you ever encountered a “500 internal server error” while browsing so website? That's it.
The main difference is that you can't do any better when using exception because you can't easily list all possible exceptions that can reach the root, while you can in languages with sum type.
Of course there will be lazy programmers in any language, the problem with exception is that even if you aren't lazy, you cannot really do better than people who are (unless you can disable exceptions, like in C++ …).
If you're writing an application and don't really care about the error types, just use `anyhow::Result` (or typedef your own `Result<T, Box<dyn Error>>` to avoid a dependency) as your error type and bubble everything up to main.
Then if there are cases where you want to handle specific errors… you can still do that, easily, with the help of the compiler.
Not really.
> for essentially the same result
With the huge differences that
1. it signals that things can fail, which largely doesn't exist with unchecked exceptions unless you re-add that information which is going to lead to similar amounts of "characters added" (e.g. “throws”)
2. you're explicitly opting into the costs and information-discarding effects, which is rather important for the language's purported goals, it supports that use-case without discarding its other important use-cases
> exceptions were introduced because bubbling things up explicitely is a pain !
Not really either, exceptions were introduced to structure error handling rather than use ad-hoc and generally in-band methods, or unrestricted jumps. They also provided a very useful hook point to provide contextual information (by default or otherwise) which the aforementioned ad-hoc methods didn't account for e.g. not much contextual data you can bundle in a shell or C error code.
Leveraging sum types for error handling is an oddly recent development, both because most languages don't have sum types, and because that it could be used thus doesn't seem like a very old realisation at large (just look at error handling in Haskell).
> Just write the happy / known-to-the-programmer path and let what you don't know about bubble up to main() or the top of your event loop.
I believe that is the point, an IO exception o some sort is a known exception case so the return type of the function should express that. This function can return either the contents of a file or an IOError if the file cannot be read for some reason. The caller can then decide whether they want to try and handle the exception case or ignore it and let it bubble up the stack, but the function should clearly express that it is a possible outcome.
Depends on the ergonomics - if it's easy to convert errors to panics then you get that in application code while also explicitly enumerating error cases as an API author.
I haven't used Rust enough seriously to comment authoritatively on this but it seems like a good trade off - I hate it when the library I'm using doesn't document error cases
With this approach, error handling is pretty minimal. All I have to think about is exception-safety. And that's an easy thing to reason about, when language does not do manual memory management.
I understand that this approach is not suitable for every software field. If I'm writing martian rover software, I don't want to crash my software, when rover might be physically destroyed.
Of course some exceptions are not exceptional and they are part of normal flow. But those situations are rare and try-catch is perfectly fine to handle those errors as well. I'm not forced to handle them, that's right. But even if I did not think about it, I'll get crash, I'll get logs, I'll get user complaints and I'll implement that separate handling. Probably it's corner-case anyway, so that's not a big deal.
And a big plus is that I don't have to think about exceptional situations when I'm writing my code. That reduces cognitive load and allows me to focus on main logic, rather than thinking about every possible outcome of every called function. And main logic is why I'm writing a software in the first place.
Unchecked exceptions are untyped indeed. They are like JavaScript on top of typed language. Well, I'm not strict-type purist. I'd use Haskell if I were one, I guess. Dynamic typing is popular for a reason. May be it would be an interesting project to implement strictly-typed exception handling. But Java did it wrong. I don't know how to make it right. If someone would make strictly-typed exception handling natural and friction-less to use, I'm all for that. But I did not see that kind of a system anywhere. And untyped works for me good enough.
In many applications errors are dispatched to some common handler far above the call stack. This is also the justification for uncheked exceptions in C# given by Anders Hejlsberg.
Not providing any value judgement but… this seems completely irrelevant to the topic. Rust has long-settled on not exceptions, and they are largely incompatible with the language's stated goals and purpose.
> Rust's panics are similar to exceptions
Except for the part where their very existence is optional.
> so for me the ideal implementation should include some short syntax to allow converting from Result to result value and throw panic for errors
You mean like `Result::unwrap` which has existed since before Rust 1.0 was a thing?
> but also allow hierarchy of panics and try-catch handling of those panics (so it would throw some specific panic, not just common panic).
I don't want to be too forward about it, but that's a language which really has nothing whatsoever to do with Rust: the "hierarchy of panics" alone is completely disqualifying so… I see even less what that comment's doing in here.
I think that it's possible to build a bridge from Result to panics, thus allowing a developer to choose a particular style of error-handling. I don't understand why exceptions are incompatible with language stated goals.
> Except for the part where their very existence is optional.
But that's up to developer to decide, right? By default panics do work. If I want to call `abort()` on panic, I have to explicitly configure it.
> You mean like `Result::unwrap` which has existed since before Rust 1.0 was a thing?
Yes, but with improved panics (including hierarchy of panics, capturing stacktrace) and enhanced language syntax to better integrate that model into a language, rather than discouraging people from using it.
Because they require significant runtime support.
> But that's up to developer to decide, right? By default panics do work. If I want to call `abort()` on panic, I have to explicitly configure it.
Not necessarily. It's the user of a library which decides whether they allow panics or not, so libraries can not assume panics will be available at all.
> Yes, but with improved panics (including hierarchy of panics, capturing stacktrace)
panics already capture stacktraces (if you run with the proper envvar enabled). As to hierarchies of panics… how do you propose to do that in a language without inheritance? Adding inheritance?
> and enhanced language syntax to better integrate that model into a language, rather than discouraging people from using it.
But the language should discourage people from using it. Having multiple ways of doing errors is actively bad, especially when one of them is completely unnecessary and plain doesn't work in many environment.
Yes and no. If you need either semantic, you need to specify it, so that when others use your code, they ensure they don't get something that's broken. They "work by default" in the sense that that's the default behavior, but people don't rely on it because unless you say you require it, they may use your code without them.
panic=abort is also a somewhat commonly suggested way of slimming down binaries.
Completely agree here.
What we dislike is rust fanboys spreading misinformation about GC, misinformation about exceptions, misinformation about mutability..
As far as I know rust implements that with the '?' syntax.
match foo {
Ok(v) => v,
Err(e) => return From::from(e)
}
What "converts a Result into a value and panics on error" is Result::unwrap[0], or Result::expect[1] (for a custom panic message).[-1] as of rustc 1.46 anyway, I don't know how "try blocks" will effect `?`, though possibly the combination of "try blocks" and unwrap would make it easier to just panic an entire function: currently each Result site has to be unwrapped by hand whereas otherwise you could just `?` every site normally and unwrap the entire block I guess?
[0] https://doc.rust-lang.org/std/result/enum.Result.html#method...
[1] https://doc.rust-lang.org/std/result/enum.Result.html#method...
Joking aside, why did Rust not adopted type constructors for a start?
Having "Error Exception Int" would not let you ignore the error when you try to get that Int. Having "TChan x -> STM x" would not let you perform channel reading outside of transaction with that channel. Monadic binding will not let you mix Error and STM computations easily while preserving sequentiality of code.
Even Kotlin got that, for all PL theory sake. The language that is very close to Java. I cannot see why Rust can't.
PS And things from this comment [1] would be very much explainable through "follow the types".
Sort of, a "project group" is a means for organizing people to help accomplish tasks in the language. (or other parts of the project in general, this group is under the libs team, which maintains the standard library)
> Joking aside, why did Rust not adopted type constructors for a start?
It is not trivial to adopt advanced type system features in a language like Rust, and we have been working on getting to this point. It just takes a lot of time. Associated type constructors are called "generic associated types" in Rust, and are desired, and being worked on, it takes non-trivial effort. Higher kinded types are less clear. Monadic binding even less clear than that.
There were some attempts to do so, for example, [1] and associated code at [2].
[1] http://web.engr.oregonstate.edu/~walkiner/student-theses/mcg...
[2] https://github.com/lambda-land/OwnershipMonad
I actually am slightly sad that Mozilla started Rust effort instead of embedding what they wanted/needed to do into some proof assistant or Haskell.
I would actually like GHC be a Rust compiler too. I think that would force it to be more modular, and if it's more modular and has more incremental CI, we'll get a lot of productivity and such a thing would actually be practical. So just gotta find a way to unwind the causality loop they (probably modularity for modularity's sake.)
Linear Haskell is a disgrace, glad you asked. It can be implemented as a library, as usual [1], and the process of inputting that linear logic into compiler I see as political, not technical.
[1] https://jpaykin.github.io/papers/pz_linearity_monad_2017.pdf
Compiler (ghc) can recognize what code that monad from [1] produces and apply transformations accordingly. The bonus here? That recognition process can accidentally optimize parts of program that do not use linear monad at all but happen to have same structure and same satisfied constraints.
About fifteen-twenty years ago I tried to invent a linear programming language or add that to Haskell. And it was not pretty. Linearity pops where you clearly don't want it to be, preventing clear explanation of ideas and/or requiring several types for innocent expression (there's no possibility of most general type in some variants of linear logic).
A proof assistant isn't magic fairy dust, and Haskell is not suited for the kind of work Rust focuses on for a variety of other reasons.
I think you did not think about embedding language into Haskell. Most of, if not all, machinery in Rust can be embedded into Haskell and used from there.
But I think you will not go that path. That's okay, I see that you prefer to design languages instead of writing libraries. Fair choice, I concur.
The most obvious one is the mandatory runtime.
EDIT: those projects you cite are effectively languages on their own, you're not running Haskell on the device, you are writing a DSL in Haskell that compiles to something else entirely. That is an approach that can work, but most of the ones I know about had some limited success and then have basically died out. Atom was last released in 2015, and has had only a few commits since then. Similar projects like Ivory have also petered out, in my understanding.
[1] http://cufp.org/archive/2008/slides/HawkinsTom.pdf
I have managed to compile Haskell code into C, Java and, finally, VHDL for some hardware simulation project. All that code did not require any kind of runtime except what was needed by typical program in that language.
You seem to completely throw away whole point about embedding language into Haskell. I am okay with that. Do you?
This kind of regular condescension from the fanbase is a great reason not to, for example.
[1] https://www.slideserve.com/kin/controlling-hybrid-vehicles-w...
After embedding something in Haskell you can have your hard real time code with all kinds of runtimes seamlessly integrated with other code in typical Haskell with its RTS.
All things associated type related play less nice with type inference, and in fact Haskell's associated types are seriously flawed and needs to overhauled.
I don't know why Rust doesn't got for HKTs. It would be relatively easy, and they have often have much nicer error messages than associated types.
Rust leans heavily on associated types in general, and there are several concrete use cases where generic associated types are sorely missed (especially involving lifetimes).
You might ask, why not both? Part of the reason is conservatism about language features, and wanting 'only one way to do it'. But another part is that Rust is starved for compiler implementation manpower. The current generic associated types RFC was proposed in 2016 and merged in 2017, and it's a highly prioritized feature. But implementation work didn't even start until sometime in 2019 (AFAIK), and it still has a ways to go.
There's also another, somewhat more mundane problem...
Higher-kinded types somewhat conflict with default type parameters. If you have
struct Foo<T = i32> { t: T }
then you can currently write Bar<Foo>
and it's equivalent to Bar<Foo<i32>>. So how do you distinguish that from passing the Foo constructor itself?It may be possible to just distinguish based on context, but it's an issue.
> Part of the reason is conservatism about language features, and wanting 'only one way to do it
But for the things HKTs are good for, it's much more usable: easy to teach, simpler errors. Dare I say HKT is the conservative choice.
> But another part is that Rust is starved for compiler implementation manpower.
That's a bummer, and I wish you the best in finding new sources of funding. But if HKT is easier, that's another reason to do it first.
> There's also another, somewhat more mundane problem...
I don't think either of us thinks that is insurmountable :)
> I don't know why Rust doesn't got for HKTs.
In my understanding, the benefits are less clear. You don't just get Monad for free because you have HKTs. ATC/GATs are much more clearly needed.
To pick a use case dear to Rust, it's definitely the best way to e.g. write data structures that are parameterized over say Rc, Arc, or some Gc.
Again, I say "the best" with pedagogy and productivity in mind: the error messages are better and intent clearer with this mechanism over the associated type alternatives.
> Having "Error Exception Int" would not let you ignore the error when you try to get that Int.
just sounds like a description of Result, which of course Rust already has.
Haskell's Functor class sets typechecking constraints on something that can contain different types but not a realisable type in itself:
class Functor f where fmap :: (a -> b) -> f a -> f b
Here, "f a" is a type, for example, list or maybe or "Map key a". But constraint is on "f", a type constructor variable (an application of type "a" to a type constructor "f" gives you a type "f a": Int is a type, List is a type constructor and List Int is a list of integers).
The same goes to monads - they are type constructors (can hold different types) with a class constraint (must implement two or three functions).
I think you cannot write something like that in Rust:
f :: Monad m => m Int -> m Int -> M String
f getA getB = do { a <- getA; b <- getB; return (show (a+b))}
this code will work over Result, Maybe, List, IO, STM and many other things.Indeed, Rust cannot do this yet, although it will be able to do something like it (but uglier) soon using associated type constructors (something like Haskell's type families).
However, I doubt this will have much impact on error handling.
For example, from your earlier post:
> PS And things from this comment [1] would be very much explainable through "follow the types".
> [1] https://news.ycombinator.com/item?id=24526604
They already are explainable that way. I went through and mapped the bullets there to the actual language/library features being explained in the corresponding blog post:
- A method on Result
- A method on Result
- A method on Result
- The ? operator
- Existential (boxed) types
- The Any trait
- API design tips
- How to create an ADT and implement some traits for it
- The ? operator uses the From trait
- Pattern matching on ADTs
In short, most of it is explaining either standard library functionality, or general-purpose language features as applied to the error-handling use case. The exception is the one language feature specific to error handling, the ? operator.
https://docs.rs/stm/0.1.2/stm/ - the user of STM must checks for the absence of side effects, not the compiler.
How about that? Will Rust compiler be able to prevent user from executing side-effecting code in STM context? Will user of Rust compiler have the same ease as with Haskell's monads?
Rust is not a pure language so no, side-effects are not part of the type-system at all.
> just sounds like a description of Result, which of course Rust already has.
That would be "Either Exception Int", I believe they were talking about Control.Monad.Error. But maybe you are right!
So I expect that's an indication of what's likely to come out of this.
But that's basically giving up on informative library-specific error types. It may a valid solution, but it is quite a big leap.
Another tack may be to have the functionality of thiserror in the standard library, to make it less of a hassle to define errors.
Technically the stdlib could probably define
Result<T, E=Box<dyn Error>>
That way there wouldn't strictly be any loss, you'd just get a more convenient "application-level" Result OOTB.I don't know if that would be incompatible with the existing though.
The prelude lives in and uses std, and I don't see people who want exceptions using no_std anyway.
It's what anyhow does:
pub type Result<T, E = Error> = core::result::Result<T, E>;
That way you can still use `Result<i32, MyCustomError>`.I don't know if there are limitations or issues with this approach though.
The only other C-family language with that syntax (dot followed by keyword) I know of is Java (where you can write foobarbaz.class; "class" is a language keyword) and it always looked weird.
The Rust maintainers found a great use for it: getting chaining (foo.await?.bar()?.await...) without adding a new postfix "sigil".
https://doc.rust-lang.org/nightly/unstable-book/language-fea...
Seriously, otherwise you can't do error handling within the body of a function that doesn't return Result<>. The language shouldn't force refactoring decisions on the programmer like that.
Sure you can, just use an inner closure that does return Result<>, and use `match` or the like for error handling. It's not any more verbose than this `try {} ... catch {}` special syntax.
Seriously? I think this is way less readable:
(|| -> Result<_,_> { bar(foo()?) })()
than this: try { bar(foo()?)? }
Also try_blocks don't have to have a "catch". It's just five characters: "t" "r" "y" "{" "}". I don't see a need for "catch" either. (|| -> Result<_,_> { Ok(bar(foo()?)?) })()
to be equivalent, since bar's Err type might need a From::from conversion via `?`. So the try-blocks version is even better in comparison.That said, if you include the `-> Result<_,_>` annotation in the closure case, you also need to include the `: Result<_,_>` annotation for the binding that the try-expr is being assigned to, otherwise the compiler won't know which impl of `std::ops::Try` to use. Of course, you can also use an annotation on the binding in the closure case and get rid of the annotation entirely. That is, the comparison is of:
let r: Result<_, _> = (|| Ok(bar(foo()?)?))();
vs let r: Result<_, _> = try { bar(foo()?)? };When you see "try{}" it says "this is here because I want to turn on ?-handling within this block".
When you see a closure formed and then immediately applied it says... well I dunno. Frankly it seems to say "this expression should be simplifiable". In fact it's a huge wart on Rust that beta-reduction doesn't preserve well-formedness of expressions.
Also if there were more stuff in the expression body it might not even be obvious at a glance that the closure is being formed-and-then-immediately-invoked, leading a reviewer to wonder "wtf is a closure doing here?".
fn foo(cond: bool, s: String) {
if cond {
drop(s);
}
else {
drop(s);
}
}
... compiles file even though `s` is being consumed in two different locations, because the compiler is aware of the control flow of if-then-else so it knows only one arm can be executed.If you tried to replace the bodies of the two branches with closures, it would not compile, because both need to consume the String. That is,
fn if_(cond: bool, t: impl FnOnce(), f: impl FnOnce()) { if cond { t() } else { f() } }
fn foo(cond: bool, s: String) {
if_(cond, || drop(s), || drop(s));
}
... won't compile. Even though only one of the two closures will be executed, both closures need to be created regardless, which means both need to own the String, which is not allowed.This problem came up all the time in pre-async-await futures 0.1 code because async code had to be written with futures combinators that all used closures, so even in cases where the programmer knows only one closure will be invoked, they would still need to clone their values so that each closure could own them. Even in regular non-async code, it often comes up with the combinators like `Result::map_err`, like:
enum Error {
Io { path: PathBuf, inner: std::io::Error },
Deserialize { path: PathBuf, inner: serde_yaml::Error },
}
fn read_config(path: PathBuf) -> Result<Config, Error> {
let f = std::fs::File::open(&path).map_err(|err| Error::Io { path: path.clone(), inner: err })?;
let c = serde_yaml::from_reader(&f).map_err(|err| Error::Deserialize { path, inner: err })?;
Ok(c)
}
Notice the `path.clone()` in the first Result's map_err closure, which is needed even though the programmer knows there is nothing else that will use `path` once that statement executes. That's because, once again, both `map_err` closures need to be created regardless of whether they execute or not, so for the second closure to be created it must own `path`, and thus the first closure needs to clone it. The equivalent code with explicit `match` statements for each Result expr would not need to clone. Compare `read_config` and `read_config2` in https://play.rust-lang.org/?version=stable&mode=debug&editio... let result: Result<i32, ParseIntError> =
match ("1"::parse<int32>(), "2"::parse<int32>(), "3"::parse<int32>()) {
(Ok(x), Ok(y), Ok(z) => Ok(x + y + z),
(Err(x), _, _) => Err(x),
(_, Err(y), _) => Err(y),
(_, _, Err(z)) => Err(z),
}
...
when the err is the same in every case (or coercible), by providing access to the ? without returning the function in fullIt doesn't seem to me that it enables any new capability -- just a convenience.
let result = {
let a = a_thing();
let b = another_thing();
let c = a.foo(b)?;
if c.a_thing() {
Ok(c)
} else {
Err(…)
}
};
which currently has to be written let result = {
let a = a_thing();
let b = another_thing();
a.foo(b).and_then(|c|
if c.a_thing() {
Ok(c)
} else {
Err(…)
}
)
}
which doesn't look too bad for a simple example but can get out of hand if you need multiple thus, and as mentioned above the compiler can start being an issue if you work across-functions a lot.In the past I would have returned an enum or just int with values for every error that could occur. But I realized that I almost never actually handle the error cases differently. I just want to know whether an error did or did not happen.
This style gives me 99% of what I need, and it's so god damn simple.
Opening a file is probably the most mundane thing I can think of, and there's at least two situations I want to log/bubble up - file not found vs some other i/o error.
Oh, setters and getters I guess return bool works ok.
Like, I don't know, discussions around generics in some future Go?
Hell, 4 out of the 5 goals stated in this announcement aren't even about changing anything but rather about mapping out the existing landscape and clarifying the current best practices, which by definition are already in use.