Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

single expression function syntax #3369

Closed
scarf005 opened this issue Jan 7, 2023 · 7 comments
Closed

single expression function syntax #3369

scarf005 opened this issue Jan 7, 2023 · 7 comments

Comments

@scarf005
Copy link

scarf005 commented Jan 7, 2023

Summary

allow kotlin-style single expression function syntax in form of fn name() = expr

fn hello() -> &'static str = "Hello"

fn add(a: i32, b: i32) -> i32 = a + b

fn rank(n: i32) -> i32 = match n {
    1 => 1,
    2 | 3 | 4 => 2,
    13..=20 => 3,
    _ => 4,
}

example implementation

Advantages

one less brackets improve readability, espcially for functions with single match block.

Disadvantages

being able to do same things in two different way may be confusing.

fn add(a: i32, b: i32) -> i32 = a + b
fn add(a: i32, b: i32) -> i32 { a + b }

Previous Discussion

there was previous discussion on this topic, but the it was mostly about implicit return type. however, that is beyond this RFC's scope.

@konsumlamm
Copy link
Contributor

(was unable to implement match)

What do you mean? Your example using match works just fine with the macro you provided.

@scarf005
Copy link
Author

scarf005 commented Jan 7, 2023

Your example using match works just fine with the macro you provided.

yeah, that was an mistake. updated example code to also demonstrate match.

@SOF3
Copy link

SOF3 commented Jan 9, 2023

I am doubtful how the former looks "better" than the latter.
image
Even from looking at the code appearance, it just saves one space and one symbol.
Also... we need a trailing semicolon right? So it is actually

fn add(a: i32, b: i32) -> i32 = a + b;
fn add(a: i32, b: i32) -> i32 { a + b }

so the real difference is that it saves one space...
Furthermore, rustfmt already has an option that accepts short block bodies to be compacted into one line.

However I do agree that the match example is cosmetically better. However, since the function signature of Rust functions tend to be way more complex than Kotlin functions, there may be some unexpected impacts, such as:

fn foo<T: ops::Deref<Target = str>>(bar: T, qux: impl Iterator<Item = (T, i32)>, corge: impl Fn() -> bool) -> bool = loop {
    if corge() {
        break false;
    }
    if let Some((v, _)) = qux.next() {
        if v == bar {
            break true;
        }
    }
}

Does it look obvious to you from the signature line that there is a = loop at the end?

And there are also where clauses. Should they fit before or after the expression?

@scarf005
Copy link
Author

scarf005 commented Jan 9, 2023

I am doubtful how the former looks "better" than the latter.

fair point. that's the reason why I've added that to disadvantages section.

we need a trailing semicolon right?

since statement ends with semicolon, and expression ends without semicolon, the syntax musn't have trailing semicolon. example

since the function signature of Rust functions tend to be way more complex than Kotlin functions, there may be some unexpected impacts.
...
Does it look obvious to you from the signature line that there is a = loop at the end?

it's a matter of formatting, it could be written as

fn foo<T: ops::Deref<Target = str>>(
    bar: T, 
    qux: impl Iterator<Item = (T, i32)>, 
    corge: impl Fn() -> bool
) -> bool = loop {
   // ...
}

And there are also where clauses. Should they fit before or after the expression?

it should go before the expression body(=), same to default syntax.

@afetisov
Copy link

afetisov commented Jan 9, 2023

This feature is more important for Kotlin, because Kotlin doesn't have Rust's "returned trailing expression" rule. This works in Rust:

fn f(x: Foo) -> Bar {
    match x {
        Foo::A(y) => y,
        Foo::B(z) => bbb(z),
        Foo::C(_) => ccc(),
    }
}

In Kotlin, this would be written in one of the following ways:

// Either a return statement:
fun f(x: Foo): Bar {
    return when(x) {
        is Foo.A -> x.y
        is Foo.B -> bbb(x.z)
        is Foo.C -> ccc()
    };
}
// Or several return statements insinde of `when` block:
fun f(x: Foo): Bar {
    when(x) {
        is Foo.A -> return x.y
        is Foo.B -> return bbb(x.z)
        is Foo.C -> return ccc()
    }
}

Note that there is more syntactic noise, and an ambiguity on the placement of return statements. The expression assignment allows to avoid both of those problems:

fun f(x: Foo) = when(x) {
        is Foo.A -> x.y
        is Foo.B -> bbb(x.z)
        is Foo.C -> ccc()
    }

This is simpler, and also allows to omit the return type. But the equivalent in Rust wouldn't be much simpler, as @SOF3 notes above. We also wouldn't want to allow omitting the return type of the function, since this complicates type checking and turns it into a global, rather than function-local, problem.

The expression body in Kotlin also increases the symmetry between functions and val/var items. Any property is really a function (or pair of functions, get/set), and the simple expression syntax is just sugar for the common case of an immutable property. Any function without parameters (other than this) can be converted to a read-only property without any loss of generality. For these reasons maintaining the symmetry between them at the syntactic level makes sense.

Nothing like that exists in Rust. There is no concept of "property". Any callable is always explicitly demarcated from variables, constants and statics, because knowing that arbitrary code may be executed is considered vital information. Moreover, arbitrary sequence of statements can be used to compute the value of constant/variable, unlike in Kotlin, and that syntax itself parallels the funciton body syntax:

const FOO: Foo =  {
    let x = call_one();
    let y = call_two(x);
    call_three(y)
};

So from that perspective, insisting too on a symmetry between function definitions and variables is counterproductive, and it already exists. We already have expression as a function's body today in current Rust, it's just that this expression must always have block expression form, for parsing simplicity reasons.

fn $f( $($args),* ) -> $ret  $expr
// where $expr is the block expr computing the function

And instead of avoiding nested block for more complex block expression as

fn f() -> T = async { .. }

we write it even simpler, as

async fn f() -> T { .. }

@Kixiron
Copy link
Member

Kixiron commented Jan 9, 2023

since statement ends with semicolon, and expression ends without semicolon, the syntax musn't have trailing semicolon.

For one, the entire feature shouldn't be predicated on what can or cannot be implemented as a macro. For another, you simply have to add a semicolon to the macro

@scarf005
Copy link
Author

closing since it seems to have more disadvantages to the new syntax.

@scarf005 scarf005 closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants