Idea: universal pipelining (a.k.a. making @await generic)

Small clarifications:

UFCS is calling a method like a function (unified function call syntax), e.g. Vec::into_iter(list). We have UFCS today. UMCS (unified method call syntax) is the idea that basically turns . into the pipeline operator properly, allowing you to do list.Vec::into_iter.

. already is the “pipelining” operator in Rust, just one that has minimal access to type-dispatched names. Thus, using it for more general pipelining (UMCS) makes some sense. That said, using some other symbol such as the |> pipe makes sense as well, to separate that pipelining from the type-based lookup, if it weren’t for the macro compatibility hazard.

That said, the treatment of await as a function in terms of pipelining is telling. It’s an operation applied to a value, not a transformation of code.

await actually cannot be properly implemented simply as a macro, even if you ignore the keyword/identifier/name-resolution/stability issues. This is not just because of diagnostics, though those are a part of the reason. The real reason is that yield isn’t (shouldn’t) be valid in an async context, only “await” is.

async fn(⟨args⟩) -> ⟨ret⟩ {
    ⟨body⟩
}

is defined to (roughly) expand to

fn(⟨args⟩) -> ⟨ret⟩ {
    async {
        let ⟨args⟩ = ⟨args⟩;
        ⟨body⟩
    }
}

and an async block is defined to create an anonymous impl Future that can use await to drive child futures. The fact that async uses generators is an implementation detail.

Being a macro means that writing the macro is exactly (modulo hygeine) the same as writing the internal code. There is no equivalent code that can be written to drive a Future from an async context.

It could be a macro-like syntactic structure. (asm! is an (unstable) one today.) But it cannot be a macro.

Also, saying await isn’t a function is also not fully correct. It doesn’t effect control flow the way ? does, nor does it do manipulation of the input code like a macro. await is best described as a method of Future with a really bizarre calling convention. All it does semantically is a potentially long-running operation and parking the current “thread” of execution while waiting.

Arbitrary code can run while you’re “blocked” on the “subroutine” to finish, whether you’re “synchronous” or “asynchronous”. It’s just that in a “synchronous” environment the level of parallelism is OS threads, and in an “asynchronous” environment, the level of parallelism is tasks decoupled from OS threads. Rust’s safety (mutability xor aliasing & Send/Sync) protect you from problems here today.

(For the record, my personal favorite option is to make await a member of Future, either as a “extern "rust-async" fn” or as a member macro (like syntactic structure), and have it go through regular name resolution to be used (i.e. not a keyword). I realize that this is unrealistic, however. My next favorite option is just any of the various postfix options, no preference within.)

4 Likes