summaryrefslogtreecommitdiff
path: root/src/blog/try-catch-in-rust.kuht
diff options
context:
space:
mode:
Diffstat (limited to 'src/blog/try-catch-in-rust.kuht')
-rwxr-xr-xsrc/blog/try-catch-in-rust.kuht553
1 files changed, 553 insertions, 0 deletions
diff --git a/src/blog/try-catch-in-rust.kuht b/src/blog/try-catch-in-rust.kuht
new file mode 100755
index 0000000..63734cd
--- /dev/null
+++ b/src/blog/try-catch-in-rust.kuht
@@ -0,0 +1,553 @@
+<import "base.kuht" as "base" />
+
+<head>
+ <title>How to Introduce try-catch to Rust</title>
+ <meta name="description" content="Don't do this, but the door is open for it." />
+</head>
+
+<body>
+
+<article>
+
+<h1>How to Introduce try-catch to Rust</h1>
+
+<p>
+ As a disclaimer, this isn't my personal proposal or anything. But I took a look at the RFC for try
+ blocks in Rust, and I thought it was interesting how it it left the door open for catch blocks in the
+ future. I thought it would be an interesting idea to share. But in order to talk about this, we have
+ to work our way forwards.
+</p>
+
+<h2>Above the Iceberg: Rust enums</h2>
+
+<p>
+ Many of you readers will already understand this much, but if you've never worked with Rust before,
+ I'll have to introduce the enum system. Rust's enums can carry values on particular variants. They're
+ pretty much just tagged or discriminated unions.
+</p>
+
+<pre>
+pub enum JsonValue {
+ Null,
+ Boolean(bool),
+ Number(f64),
+ String(String),
+ Array(Vec&lt;JsonValue>),
+ Object(Vec&lt;(String, JsonValue)>),
+}
+</pre>
+
+<p>
+ There are a few ways we can get the inner value. We can use a match expression, which is a bit like
+ switch in other languages.
+</p>
+
+<pre>
+fn to_json(value: JsonValue) -> String {
+ match value {
+ JsonValue::Null => "null".to_string(),
+ JsonValue::Boolean(b) => b.to_string(),
+ JsonValue::Number(n) => n.to_string(),
+ JsonValue::String(s) => format!("\"{s}\""),
+ JsonValue::Array(a) => {
+ let mut builder = String::from("[ ");
+ for element in a {
+ builder.push_str(&to_json(element));
+ builder.push_str(", ");
+ }
+ builder.push(']');
+ builder
+ }
+ JsonValue::Object(o) => {
+ let mut builder = String::from("{ ");
+ for (key, value) in o {
+ builder.push_str(&format!("\"{key}\": {}", to_json(value)));
+ builder.push_str(", ");
+ }
+ builder.push('}');
+ builder
+ }
+ }
+}
+</pre>
+
+<p>We can use if let to match on only one particular variant.</p>
+
+<pre>
+fn unwrap_number(value: JsonValue) -> f64 {
+ if let JsonValue::Number(n) = value {
+ n
+ } else {
+ panic!("The passed value was not a number");
+ }
+}
+</pre>
+
+<p>
+ More recently, we got let else, which lets us assert that it is one particular variant, or quit early.
+</p>
+
+<pre>
+fn unwrap_number(value: JsonValue) -> f64 {
+ let JsonValue::Number(number) = value else {
+ panic!("The passed value was not a number");
+ }
+
+ number
+}
+</pre>
+
+<p>But importantly, there's no way to get the inner value without first checking for the variant.</p>
+
+<h2>In the Beginning: The <code>Result</code> type</h2>
+
+<p>
+ Rust doesn't have exceptions. The <code>panic</code> macro, shown earlier, causes an immediate end to
+ the program (in most cases). For catchable errors, we use the <code>Result</code> type, which is
+ included in Rust's core library.
+</p>
+
+<pre>
+pub enum Result&lt;T, E> {
+ Ok(T),
+ Err(E),
+}
+</pre>
+
+<p>
+ The <code>Result</code> type has two generic types: <code>T</code>, which is the type that was
+ expected to be returned; and <code>E</code>, which is the type that will be returned instead if an
+ error occurred. For example, we could define a <code>parse_number</code> function like this:
+</p>
+
+<pre>
+fn parse_to_f64(s: &str) -> Result&lt;f64, ParseFloatError>
+</pre>
+
+<p>
+ If the passed string is a number, then this function will return a 64-bit float. Otherwise, it will
+ return a <code>ParseFloatError</code>, which is an error describing a failure to parse the number.
+ We can use the result of this function like so:
+</p>
+
+<pre>
+fn foo() {
+ let number = match parse_to_f64("3.14159") {
+ Ok(number) => number,
+ Err(e) => panic!("{e}"),
+ };
+
+ println!("{number}");
+}
+</pre>
+
+<p>
+ The <code>Result</code> type has a convenient helper method to do this, called <code>unwrap</code>.
+ If the result is <code>Ok</code>, then the inner value will be returned. Otherwise, the program will
+ panic.
+</p>
+
+<pre>
+fn foo() {
+ let number = parse_to_f64("3.14159").unwrap();
+ println!("{number}");
+}
+</pre>
+
+<p>But typically, you'd want to just throw the error again.</p>
+
+<pre>
+fn foo() -> Result&lt;(), ParseFloatError> {
+ let number = match parse_to_f64("3.14159") {
+ Ok(number) => number,
+ Err(e) => return Err(e),
+ };
+
+ println!("{number}");
+}
+</pre>
+
+<h2>Introduced Later: The <code>?</code> operator</h2>
+
+<p>
+ As you might imagine, using that match expression over and over again to propogate errors will
+ quickly become tedious. The original solution was to use the <code>try</code> macro to do it for you.
+</p>
+
+<pre>
+fn foo() -> Result&lt;(), ParseFloatError> {
+ let number = try!(parse_to_f64("3.14159"));
+ println!("{number}");
+}
+</pre>
+
+<p>
+ But the 2018 edition of Rust introduced some syntactic sugar to make this even easier. It can be used
+ to propogate an error in any function which returns a similar <code>Result</code>.
+</p>
+
+<pre>
+fn foo() -> Result&lt;(), ParseFloatError> {
+ let number = parse_to_f64("3.14159")?;
+ println!("{number}");
+}
+</pre>
+
+<p>It can even convert between errors, if possible.</p>
+
+<pre>
+enum JsonError {
+ ParseNumber(ParseFloatError),
+ Other(String),
+}
+
+impl From&lt;ParseFloatError> for JsonError {
+ fn from(value: ParseFloatError) -> Self {
+ Self::ParseNumber(value)
+ }
+}
+
+fn foo() -> Result&lt;(), JsonError> {
+ // automatically converts the ParseFloatError to a JsonError
+ let number = parse_to_f64("3.14159")?;
+ println!("{number}");
+}
+</pre>
+
+<p>It also works on the <code>Option</code> type, which is also included in Rust.</p>
+
+<pre>
+fn foo() -> Option&lt;i32> {
+ let array = [1, 2, 3, 4];
+ let x = array.get(4)?; // get the element at index 4, which doesn't exist in the array
+ Some(x)
+}
+</pre>
+
+<p>
+ And some of you may be surprised to learn that it also works on <code>ControlFlow</code> and some
+ <code>Poll</code> types.
+</p>
+
+<p>
+ This isn't stable yet, but there's also a proposal to allow the use of the <code>?</code> operator on
+ custom types. This would be done using a few new traits: <code>Residual</code>,
+ <code>FromResidual</code>, and <code>Try</code>. A residual is a type that can be returned from using
+ a <code>?</code>. The <code>FromResidual</code> trait is implemented on types that can be created
+ from residuals (for example, using <code>?</code> to get a
+ <code>Result&lt;Infallible, JsonError></code>, then converting that to a
+ <code>Result&lt;(), JsonError></code>). The <code>Try</code> trait is implemented when a
+ <code>FromResidual</code> type can also have the <code>?</code> operator applied to it. These traits
+ are already available in nightly Rust. As an exercise for the Dallas Rust User Group, we created
+ <a href="https://github.com/joshuaPurushothaman/diy-result">our own <code>Result</code> type</a>, and
+ used these traits to make the <code>?</code> operator work with it.
+</p>
+
+<h2>In Nightly: Try Blocks</h2>
+
+<p>
+ Today, <code>try</code> is a reserved keyword. So to use the <code>try!</code> macro in newer
+ editions of Rust, you'll need to use the raw-literal syntax.
+</p>
+
+<pre>
+fn foo() -> Result&lt;(), ParseFloatError> {
+ let number = r#try!(parse_to_f64("3.14159"));
+ println!("{number}");
+}
+</pre>
+
+<p>
+ What's the keyword being used for? It's for the try-block feature, which is currently only available
+ in nightly. The idea for this was made at roughly the same time as the <code>?</code> operator, to
+ catch anything that was propogated with it. However, only the <code>?</code> got merged.
+</p>
+
+<pre>
+let x: Result&lt;_, _> = try { foo()?.bar()?.baz()? };
+</pre>
+
+<p>This way, you can catch an error without having to create a whole new function.</p>
+
+<h2>The Future: Catch blocks</h2>
+
+<p>
+ From what I understand, the original idea was to not reserve <code>try</code> as a keyword. Instead,
+ the keyword was going to be <code>catch</code>. That idea was dropped, presumably because it would
+ break with the convention created by every other programming language. But this also leaves the door
+ open to introduce catch blocks later.
+</p>
+
+<p>
+ I haven't found any proposed RFCs for what catch blocks might look like. I think most people don't
+ want to think about this until <code>try</code> is finished. But people have noted that we have the
+ option of adding catch later, and the rough syntax isn't too hard to figure out.
+</p>
+
+<pre>
+let x: Result&lt;f64, _> = try {
+ //...
+} catch error {
+ panic!("Unexpected error: {:?}", error);
+}
+</pre>
+
+<p>We could pattern match on the expression too.</p>
+
+<pre>
+let x: f64 = try {
+ //...
+} catch JsonError::ParseNumber(error) {
+ panic!("Failed to parse float: {:?}", error);
+} catch JsonError::Other(error) {
+ panic!("An unexpected error occurred: {:?}", error);
+}
+</pre>
+
+<p>We'd also have to allow try-catch on <code>Option</code>.</p>
+
+<pre>
+let array = [1, 2, 3, 4];
+let x = try {
+ array.get(4)?;
+} catch { // note that there's no reason for a variable to be created here
+ // this would probably desugar to `catch _`
+ // that desugaring would allow this syntax to also be used with Result
+ panic!("wait, what am i doing with my life?");
+}
+</pre>
+
+<p>
+ Creating this API would probably also mean we'd have to think about adding a <code>Catch</code>
+ trait, so that it could be allowed on custom types. Modeling this is difficult, but can be done.
+</p>
+
+<pre>
+pub trait Catch: Try {
+ type Error;
+
+ fn error(residual: Self::Residual) -> Self::Error;
+}
+
+impl&lt;T, E> for Result&lt;T, E> {
+ type Error = E;
+
+ fn error(residual: Self::Residual) -> Self::Error {
+ let Err(error) = self;
+ error
+ }
+}
+
+impl&lt;T> for Option&lt;T> {
+ type Error = ();
+
+ fn error(residual: Self::Residual) -> Self::Error {
+ ()
+ }
+}
+</pre>
+
+<p>In case you're wondering about the desugaring:</p>
+
+<pre>
+let x: f64 = match try { /* ... */ }.branch() {
+ ControlFlow::Continue(output) => output,
+ ControlFlow::Break(residual) => match Result::error(residual) {
+ JsonError::ParseNumber(error) => panic!("Failed to parse float: {:?}", error),
+ JsonError::Other(error) => panic!("An unexpected error occurred: {:?}", error),
+ }
+}
+</pre>
+
+<p>
+ Does that mean that <code>finally</code> could be added in the future too? Perhaps. I've never been a
+ big fan of finally blocks. But we can do it. If you're familiar with the
+<a href="https://lib.rs/scopeguard">scopeguard</a> crate, then it shouldn't seem too difficult.
+</p>
+
+<pre>
+let x: f64 = try {
+ //...
+} catch JsonError::ParseNumber(error) {
+ panic!("Failed to parse float: {:?}", error);
+} catch JsonError::Other(error) {
+ panic!("An unexpected error occurred: {:?}", error);
+} finally {
+ println!("finally! we're done!");
+}
+
+// desugars to...
+
+let x: f64 = {
+ defer! {
+ println!("finally! we're done!");
+ }
+
+ match try { /* ... */ }.branch() {
+ ControlFlow::Continue(output) => output,
+ ControlFlow::Break(residual) => match Result::error(residual) {
+ JsonError::ParseNumber(error) => panic!("Failed to parse float: {:?}", error),
+ JsonError::Other(error) => panic!("An unexpected error occurred: {:?}", error),
+ }
+ }
+}
+</pre>
+
+<h2>Yeet! We can go even further</h2>
+
+<p>
+ Now that we have most of the syntax from languages that have exceptions, let's go even further.
+ We could consider adding the <code>throw e</code> keyword as syntactic sugar for
+ <code>return Err(e)</code>.
+</p>
+
+<pre>
+let x: f64 = try {
+ throw JsonError::Other("oh no, an error!".to_string());
+} catch JsonError::ParseNumber(error) {
+ panic!("Failed to parse float: {:?}", error);
+} catch JsonError::Other(error) {
+ panic!("An unexpected error occurred: {:?}", error);
+} finally {
+ println!("finally! we're done!");
+}
+</pre>
+
+<p>
+ This isn't available in nightly, because <code>throw</code> isn't yet a keyword in Rust. Worse, we
+ can't add it as a keyword until everybody agrees on what keyword to use. Some languages, such
+ Python, use <code>raise</code> instead.
+</p>
+
+<p>
+ This problem is known as bikeshedding. People can't agree on what color to paint the bikeshed, so the
+ bikeshed never gets built. Everybody agrees that having the bikeshed is important, regardless of what
+ color it is. A common solution to this is to pick an option nobody would ever want. This lets everyone
+ know that we're not actually going to use that color, so we can and will change it later. This
+ happened.
+</p>
+
+<pre>
+#![feature(yeet_expr)]
+
+fn throw_error() -> Result&lt;Infallible, JsonError> {
+ do yeet JsonError::Other("an error has been yeeted");
+}
+</pre>
+
+<p>
+ Yep, <code>yeet</code> is the chosen keyword. The <code>do</code> needs to be there because
+ <code>yeet</code> is not a reserved word, but <code>do</code> is, despite it not being used for
+ anything.
+</p>
+
+<p>
+ The way this works under the hood, is <code>do yeet e</code> desugars to <code>Yeet(e)</code>.
+ Then a couple of <code>FromResidual</code> implementations allow the <code>Yeet</code> type to be
+ used for error propogation.
+</p>
+
+<pre>
+impl&lt;T> FromResidual&lt;Yeet&lt;()>> for Option&lt;T> {
+ fn from_residual(_: Yeet&lt;()>) -> Option&lt;T> {
+ None
+ }
+}
+
+impl&lt;T, E, F: From&lt;E>> FromResidual&lt;Yeet&lt;E>> for Result&lt;T, F> {
+ fn from_residual(error: Yeet&lt;E>) -> Result&lt;T, F> {
+ Err(error.0.into())
+ }
+}
+</pre>
+
+<p>
+ Now that we've been using <code>yeet</code> for so long, there are some people who want this to be
+ the new keyword. There was even a proposal to reserve yeet for Rust 2021. It was rejected.
+</p>
+
+<h2>Now in Reverse? What?</h2>
+
+<p>
+ I had to share this because Rust does not have exceptions. The idea that we could introduce an
+ exception-like syntax for a language which doesn't have them is amazing to me.
+</p>
+
+<p>This got me thinking, could we introduce <code>Result</code> to a language that doesn't have it?</p>
+
+<p>Imagine the following C# code, where using try, but without a catch, returns a <code>Result</code>.</p>
+
+<pre>
+// I used a wildcard for the error type because C# can throw one of multiple exceptions here
+Result&lt;decimal, _> = try {
+ yield demical.Parse("3.14159");
+}
+</pre>
+
+<p>
+ This would be easier in languages where block expressions exist. If a language were to pursue this,
+ I'd prefer they call the type <code>Except</code> or <code>Exceptional</code> instead of
+ <code>Result</code>.
+</p>
+
+<h2>Conclusion</h2>
+
+<p>
+ The main I reason I wrote this post was because, when I proposed a new scripting language in an
+ <a href="/blog/best-tool-for-the-job.html">earlier blog post</a>, I wanted to talk about how
+ try/catch would be implemented in it. But I figured that was way too off-topic for that post. So I
+ wanted to discuss it here.
+</p>
+
+<p>
+ My scripting language would have unchecked exceptions, with the option to catch errors and put them
+ inside of an <code>Exceptional</code> type.
+</p>
+
+<pre>
+function throws() {
+ throw "oh no"
+}
+
+throws() // this crashes the script
+</pre>
+
+<p>Wrapping an expression in a try block yields an <code>Except</code> type.</p>
+
+<pre>
+function throws() {
+ fail with "oh no"
+}
+
+let x: Except&lt;any, any> = try {
+ throws() // this doesn't crash
+}
+</pre>
+
+<p>I think it's feasible to use a postfix <code>!</code> as syntactic sugar for this</p>
+
+<pre>
+function throws() {
+ fail with "oh no"
+}
+
+let x: Except&lt;any, any> = throws()!
+</pre>
+
+<p>Then with <code>catch</code> or <code>.catch</code>, the error can be handled</p>
+
+<pre>
+function throws() {
+ fail with "oh no"
+}
+
+let x: Except&lt;any, any> = throws()!.catch error {
+ print("an error occurred")
+ exit()
+}
+</pre>
+
+<p>I like this idea. I'm glad I could share it with you.</p>
+
+</article>
+</body>