Monday, July 30, 2018

Are functions colored?

There is a blog post that I read some weeks ago when I was in a frenzy reading Rust Language RFCs, specifically this one (yes, I'm crazy and read the better part of that thread; the comment that mentioned the article is here).

I want to begin with a little context because my opinions here shifted a lot while thinking about the involved topics.

First: how Rust handles exceptional conditions. In a language such as Python, you could write a function like this:
def divide(divident, divisor):
    if divisor == 0:
        raise RuntimeError("Divide by zero")
    return divident / divisor
There's probably a DivisionByZero error in the standard library, but that's not my point. The important thing is that "raising" (or in other languages "throwing") an exception can be handled by the caller, or not (badness of code again not the point):
def foo():
    try:
        return divide(3, 0)
    except RuntimeError:
        return None

def bar():
    x = divide(3, 0)
    print(x)
foo provides a default value for division by zero, bar propagates the error to its caller, who then has to handle (or again propagate) it.

Rust takes a different approach. Rust doesn't have exceptions, instead it has a Result type that's defined roughly like this:
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}
A result can either be Ok and contain a result of type T, or an Err, and contain an error value of type E. Our function could look like this:
fn divide(divident: u32, divisor: u32) -> Result<u32, ()> {
    if divisor == 0 {
        Ok(divident / divisor)
    } else {
        Err(())
    }
}
Here E is (), which means an Err doesn't carry any additional information. This is how a caller could use this:
fn foo() -> Option<u32> {
    match divide(3, 0) {
        Result::Ok(value) => Some(value),
        Result::Err(_) => None,
    }
}

fn bar() {
    let x = divide(3, 0);
    println!("{}", x.unwrap());
}

fn baz() {
    if let Result::Ok(x) = divide(3, 0) {
        println!("{}", value);
    }
}
Here the discrepancies between Python and Rust are very apparent:
  • foo returns Option<u32>, not just u32. That means the caller again has to unpack that value if they need a u32. The Python code would probably lead to an exception later, if None can't be processed somewhere.
  • bar uses a second error handling mode that is kind of similar to exceptions, but is not the first choice in Rust: it panics. unwrap() returns the value of a result, but if the result was an error, the panic (usually) kills the thread or process. Panics can be caught, but that's meant for infrastructure code such as thread pools, not for everyday application error handling.
  • baz swallows the exception: the if let statement matches only a successful result, so if there's no match, nothing happens.
There is an additional way of handling errors that I'll need again later:
fn quuz() -> Result<(), ()> {
    let x = divide(3, 0)?;
    println!("{}", x);
    Ok(())
}
Notice the question mark. This operator - I'll call it the try operator - returns the result immediately if it was an error, or takes its content if it was successful. This early-returning behavior feels a little like raising an exception, but it requires that the function itself returns a result - otherwise it couldn't return an Err! A side effect is that, even though we don't have a real result, we need to end the function with Ok(()), i.e. return a successful result without any data.

Now back to code colors. This is the point where you should read the linked blog post, if you haven't already. It proposes a language where
  • every function is either blue or red
  • those colors are called with different syntax
  • only red functions can call other red functions
  • red functions are "annoying"
Those red functions are async functions that need async/await syntax to call. For the rest, read the linked post. An important point here is transparency: callers have to know function color; higher-order functions may work on all blue functions, but a separate function for red functions (and thus code duplication) might be necessary.

At first the argument that async poses a needless separation into two colors seemed intriguing, although I wasn't entirely convinced. But that doubt felt lazy, caused by habit rather than reason. I left it at that, followed some more discussions about possible syntactic options for async/await in Rust, and wrote some actual async Rust code.

Rust tries to get things right. The people behind Rust experiment with features a lot before they're stabilized, and they wouldn't just add an await keyword because that's what other languages do; Rust's syntax should not only be familiar, it should be practical and reduce the possibility for programmer errors.

A concern with await is the combination with the question mark. Async operations are often ones that could fail, so you would await them and then check for an error - like this:
(await do_something())?;  // or
await do_something()?;
The first is ugly, but would you expect the second to await first, try second? That's why people were discussing a postfix variant:
do_something().await?;  // or
do_something().await!()?;  // actually proposed postfix macro syntax, or even
do_something()~?;  // I made that up, but in principle, why not?
This reads properly from left to right. And, now I'm getting to the point, it shows that the blue/red distinction is not really unique to the topic of async functions. Error handling, at least the explicit kind using Result, is basically the same:
  • every function is either blue or red - either you return T or Result<T>
  • those colors are called with different syntax - you need to extract Results, but not Ts
  • only red functions can call other red functions - not entirely. You can do one of three things:
    • make your function red and use the try operator (quux);
    • deal with the implications and keep your function blue (foo, bar, baz);
    • write buggy code (bar, baz depending on requirements)
  • red functions are "annoying" - Results are more annoying than Ts
Does that mean that Results are a bad way of handling errors? I don't think so. How do exceptions hold up in this comparison?
  • every function is either blue or red - if you throw exceptions, that's a difference in contract; I take it as a yes
  • those colors are called with different syntax - that's the plus, your higher-order functions don't see any color
  • only red functions can call other red functions - nope, but I think that was already debatable
  • red functions are "annoying" - not to call, but to call correctly
In fact, if we look at checked exceptions as they exist in Java, we get basically the same picture as with async/await or results: you can't put a function that may throw a checked exception somewhere no checked exceptions are expected.

So Results and (unchecked) exceptions are both valid mechanisms on a spectrum of explicit vs. implicit error handling. await is on the explicit side of the asynchronicity spectrum; it remains to be seen how implicit asynchronicity can be implemented properly (Go might qualify as an already working solution here). It may not be a good fit for Rust, which chooses the explicit path, but dynamic languages might benefit from a proven solution to this.

No comments: