We had a fun discussion in #t-lang about possible new syntax for declaring functions in Rust. There were a lot of cool ideas put forward, and while mulling them over I realized a lot of them work nicely together and can be introduced in a backwards-compatible way to give us some cool new capabilities. While these were fresh in my mind and I'm feeling excited about them, I wanted to write them in one place.

For background, top level functions in Rust look sort of like this:

fn foo(x: i32) -> i32 {
    x + 1
}

In Rust 2018, we added async fn:

async fn foo(x: i32) -> i32 {
    x + 1
}

While that one doesn't do anything particularly interesting, an async function gives you the ability to use await inside it. It also secretly changes the return type from an i32 to an impl Future<Output = i32>. This is regarded by many to have been a mistake, and it's starting to cause issues now that we have async functions in traits since there is no way to add additional bounds like Send to the return type. Anyway, async fn foo is mostly just syntactic sugar that desugars into:

fn foo(x: i32) -> impl Future<Output = i32> {
    async { x + 1 }
}

It's likely that Rust will gain a whole bunch of new keywords we can stick in front of fn in the future.1 For example, nightly Rust just got support for gen fn and async gen fn. Those desugar similar, by wrapping the return type in impl Iterator or impl AsyncIterator and wrapping the body in gen { } or async gen { }.

Another piece of sugar we could add is try fn, which is actually what started off the discussion thread today. Following the pattern we've had so far, we'd expect to be able to write something like:

try fn foo() -> i32 {
    let x = read_number()?;
    x
}

and have this desugar to:

fn foo() -> impl Try<Output = i32, Residual = ???> {
    try {
        let x = read_number()?;
        x
    }
}

The problem is we need a hint for the Residual type. The obvious thing to do would be to add something to the function header, like try fn foo() -> i32 throws E. But if you've ever looked at the Residual types for the Try impls in the standard library, you know that these can look pretty hairy and not particularly intuitive. For example, to make a function that returns an Option, we'd need to write:

try fn foo() -> i32 throws Option<Infallible> {
    let x = read_number()?;
    x
}

This would give the compiler enough information to find the Try impl for Option. But notice that we also could have just written fn foo() -> Option<i32>, which is shorter and you don't have to figure out why my fallible function has an Infallible in it.

At this point, Lukas Wirth observed that they would rather see a shorthand for functions whose body is a single expression. If we did this, we could write try fn as:

fn foo() -> Option<i32> = try {
    let x = read_number()?;
    x
}

So that's pretty neat.

This also invites us to reconsider async fn. We could instead write:

fn foo() -> impl Future<Output = i32> = async {
    let x = read_number().await;
    x
}

That's not too bad, but impl Future<Output = i32> is a bit wordy. We could come up with some rules that would let you write impl Future<i32> instead, which honestly is how we usually read that out loud anyway. But then joboet and pitaj pointed out that we could treat Trait -> Type as shorthand for Trait<Output = Type>. TC pointed out that we could probably generalize this to support yields T and Iterator<Item = T>.

So if we combined a few of these ideas, we'd be able to write:

fn foo() -> impl Future -> i32 = async {
    let x = read_number().await;
    x
}

I think this shows a lot of potential. I want to try to generalize this a bit more though. Instead of special-casing the Output associated type, we could create a set of attributes indicate an associated type can be used with trait keyword shorthands. For example, define the Future and Iterator traits like this:

trait Future {
    #[keyword(return)]
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

trait Iterator {
    #[keyword(yields)]
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

This would let us refer to Future<Output = T> as Future -> T and Iterator<Item = T> as Iterator yields T.

We could even combine them:

trait Coroutine<R> {
    #[keyword(yields)]
    type Yield;

    #[keyword(return)]
    type Return;

    fn resume(self: Pin<&mut Self>, arg: R) -> CoroutineState<Self::Yield, Self::Return>;
}

fn coroutine() -> impl Coroutine<()> -> bool yields i32 = || {
    yield 42;
    true
}

This would also let remove some of the special handling around the Fn* traits and we could expose this functionality to users so libraries could use this sugar in their own traits.

At this point, I'd like to take a step back and think about plain fn functions. Notice that the following two would be equivalent:

fn foo() -> i32 {
    let number = read_number();
    number
}

fn foo() -> i32 = {
    let number = read_number();
    number
}

One way of think of this is that we've made the = optional. But I'd like to think of it a different way. Let's say instead we think of the = form as the standard function declaration syntax. Then, if the function body consists of a single block, we can use a compressed syntax. For a regular { } block, that just looks like the function declaration syntax we're used to. But for blocks with a keyword in front, like async { } or try { }, we say the keyword moves all the way to the front of the function header. In addition, each block as an characteristic trait associated with it, so when we used the block shorthand for function declarations, we also wrap an impl Trait around the return type.

Here are some examples:

// async ////////////////////////////////////////

async fn foo() -> i32 {
    let number = read_number().await;
    number
}

// desugars to:

fn foo() = impl Future<Output = i32> = async {
    let number = read_number().await;
    number
}


// gen //////////////////////////////////////////

gen fn foo() -> i32 {
    yield 1;
    yield 2;
}

// desugars to:

fn foo() = impl Iterator<Item = i32> = gen {
    yield 1;
    yield 2;
}

// assuming `Iterator` is defined like:

trait Iterator {
    #[keyword(return)]
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}


// async gen ////////////////////////////////////

async gen fn foo() -> i32 {
    yield 1;
    yield 2;
}

// desugars to:

fn foo() = impl AsyncIterator<Item = i32> = async gen {
    yield 1;
    yield 2;
}

I've left try out because it's complicated. You could technically do something like:

try fn foo() -> i32 throws Option<Infallible> {
    let number = read_number()?;
    number
}

but for try users usually want to know the concrete type. So instead I'd expect most people to prefer the desugared form:

fn foo() -> Option<i32> = try {
    let number = read_number()?;
    number
}

Note that the "pulling the keyword forward" transformation doesn't work because this function returns a concrete type and what I've proposed here is that pulling the keyword forward always adds an impl Trait rather than a concrete type.

Anyway, I'm pretty excited about this idea.2 It feels like a consistent way to handle these connections between blocks, traits, and functions. It's backwards compatible with the syntax we have so far, but it gives us a lot more expressiveness in cases where we're currently missing it.


1

You can already do unsafe fn and const fn today, but these don't desugar in the same way as other proposed keywords here do.

2

Of course, I also just started thinking about this today and cranked out a blog post, so I may hate it by Monday.