[no-rfc] rust function format, fn foo(x: i32) -> i32 = const {

@steveklabnik did a nice writeup about rust's function syntax, prefacing it with a note that he did not intend to suggest a change of rust as it is as established as it is. but ... the ideas have charme. and, rustfmt could rewrite syntax in case ... here steves "musings" as he calls it: https://steveklabnik.com/writing/too-many-words-about-rusts-function-syntax

excerpt how rust code then would look like:

// full signature
fn foo(x: i32) -> i32 = {
    todo!()
};

// empty return type
fn foo(x: i32) = {
    todo!()
};

// const
fn foo(x: i32) -> i32 = const {
    todo!()
};
fn main() = println!("Hello, world!");
fn hello(name): fn(&str) = println!("Hello, {name}!");

i am hoping it is allowed to post this into this forum, to get comments if such ideas are thinkable, and if yes, if there are corner cases where such a syntax change could not work out well.

1 Like

To quote Steve:

I don’t think that making changes like this has enough benefits once a language is as established as Rust is.

2 Likes

This syntax has been proposed before, and in particular, it's appealing for things like = try { ... }.

2 Likes

C# has a version of this that's significantly more valuable, since it allow omitting the return: something like int Foo(int x) => this.blah + x;. Yet I'm still not fond of it because of the cost of the choice.

I'd rather it be left a formatting choice, since that example from the OP could always be written

// const
fn foo(x: i32) -> i32 { const {
    todo!()
}}

Rather than adding extra syntax for it.

(I often see macros formatted similarly to this already, for example, since => {{ … }} is common if the macro is supposed to generate a block.)

7 Likes

I feel this is just barely in the "not worth the complication" side given how clean the existing function syntax is, even if this was from a greenfield language design, unless it was the only syntax. But that means that things like where clauses etc would be much more confusing, so :person_shrugging:

Hmmm, interesting. We could make this the standard formatting of a function with a top-level argumentless block construct (unsafe, try, const, maybe loop).

This is utterly redundant.

There is a much simpler approach which has precedent in other languages (amazingly, even C++ got this right) - simply generalise the function body in the grammer to be any kind of block statement.

    fn foo(x: i32) -> i32
    const { // body is a const block
        todo!()
    }

Likewise, it could be a try block, an async block, etc..

1 Like

I don't understand what's the point.

Extending the grammar to any expression instead of any block is equally trivial, but much more useful.

I'd like to see this implemented because the complication here is so small it's basically lost in the noise, but the convenience is real.

For consistency it would also be nice to support braced bodies for const/static items with large initializers (const C: u8 { /* multi-line body */ }), but they are less useful in practice.

I agree. It would be "one more thing to teach", but at the same time, it would be several less things to teach: once you learn the = expr form, you know how to do try, or unsafe, or const, or anything else you want.

This has been proposed many times, and imho it could be a good choice if Rust were designed from scratch. That said, there are a few complications, even ignoring backwards compatibility.

  • The syntax is strongly reminiscent of setting the return value of the function to some block expression. It would be counterintuitive if you couldn't use simple expressions in this position, so these usually go hand in hand. But allowing braceless function bodies is a more complex change. In particular, it would significantly complicate the life of macro authors.

  • The same syntax should also be usable for other kinds of blocks, e.g. async { }, try { } and generators. But then for e.g. async blocks the syntax looks like you are directly returning an async block. That means that the return type of the function should be a Future, whereas currently it's the Output of that future.

3 Likes

Assuming the currently experimental inline const semantics (2920-inline-const - The Rust RFC Book):

fn foo(Args) -> Ret = const { ... };

const fn const_api(Args) -> Ret { ... }
fn const_body(Args) -> Ret { const { ... } }
const fn const_both(Args) -> Ret { const { ... } }

const_api is a const-evaluatable function of its inputs. const_body and const_both each evaluate to a const value which cannot depend on the arguments, but only the latter is a usable in const contexts. Which would foo mean, or would it be something else?

The original blog post seemed to choose the meaning of const_api, but I would expect that of const_body, especially if it was a general thing. Consider unsafe:

fn bar(Args) -> Ret = unsafe { ... };

unsafe fn unsafe_api(Args) -> Ret { ... }
fn unsafe_body(Args) -> Ret { unsafe { ... } }
unsafe fn unsafe_both(Args) -> Ret { unsafe { ... } }

Again, we have safe and unsafe functions, which can each have safe or unsafe bodies, although unsafe fn implicitly has an unsafe body for backwards compatibility (opt-out with #![deny(unsafe_op_in_unsafe_fn)]).

Basically, I feel like anything after the hypothetical = should not be part of the function signature.

3 Likes

The point is uniformity and the principle of least surprise. The = expression form adds a new variation and deviates from existing practice whereas my suggestion fits with and extends the current design of rust.

This is the same design as in control flow structures where rust dictates to always use braces and does not have a braceless single instruction variation as in other C style languages.

1 Like

I fail to see the convenience argument: you're just swapping { and } for = and ; in terms of tokens? If there's a case here it's for nicer formatting, especially of the "special" block items that has been brought up.

1 Like

Right. If you wanted to use this with async, you'd write:

fn func() -> impl Future<Output=SomeType> = async {
    // ...
    some_sometype
}
3 Likes

This proposal has "double semicolon" as weird consequence if it has the most liberal expression

Function:  fn  .... ( BlockExpression | = Expression ; | ; )

We could rewrite functions:

fn foo (x : u32) -> u32 {
    x
}

fn bar (x : u32) {
    x;
}

as

fn foo (x : u32) -> u32 = x;

fn bar (x : u32) = x;; // ????
// double semicolon ^^~~here

But i's illegal.

So we need a trait Noop (or Drop is enough) for this:

trait Noop :  Sized {
    fn noop(self) {
    }
}
impl<T> Noop for T {}

///
fn bar (x : u32) = x.noop();

P.S.

An alternative to "let" syntax is "match" syntax:

Function:  fn  .... ( BlockExpression | => Expression ; | ; )
fn foo (x : u32) -> u32 => x;

fn bar (x : u32) => x.noop();

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.