- Feature Name:
fn_body_blocks
- Start Date: 2024-05-06
- RFC PR: rust-lang/rfcs#3629
- Rust Issue: rust-lang/rust#0000
Allow the definition of functions with a single-block construct in their body, without requiring top-level braces.
Examples:
unsafe fn read_bool(x: *const bool) -> bool
unsafe {
*x
}
fn countup(limit: usize) -> impl Iterator<Item = usize>
gen {
for i in 0..limit {
yield i;
}
}
fn is_some<T>(x: Option<T>) -> bool
match x {
Some(_) => true,
None => false,
}
This provides a concise shorthand for functions which consist of a single block construct. These are relatively common and can otherwise lead to rightward drift.
Another use case comes once unsafe fn
no longer implies an unsafe { }
body
(RFC 2585). For cases where
users want the old behavior, being able to define a function with a top-level
unsafe { ... }
block avoids requiring an additional nested block.
This function body block syntax also gives a single, consistent, unified syntax
to support functions that pair with new expression blocks that are under
development, such as gen { }
, try { }
and async gen { }
. While we might
be tempted to add syntax such as
gen fn foo() -> i32 {
yield 1;
yield 2;
yield 3;
}
for each new kind of block, this becomes less necessary if we can instead write:
fn foo() -> impl Iterator<Item = i32>
gen {
yield 1;
yield 2;
yield 3;
}
(This would be further improved by syntax to simplify the
impl Iterator<Item = i32>
type.)
Functions whose entire body consists of a construct like match
that takes a
single braced block can omit the outer braces:
fn function(x: usize)
match x {
0 => println!("Zero"),
_ => println!("Nonzero"),
}
fn countdown(mut count: usize)
while count > 0 {
println!("{count}");
count -= 1;
}
The full list of block constructs permitted at the top level of a function:
unsafe
loop
while
andwhile let
for
async
match
if
andif let
, if and only if there's noelse
.try
(once it exists)gen
(once it exists)async gen
(once it exists)
By having two ways to declare a function, it is not always clear which version is preferred. This adds additional cognitive load when defining a function.
One of the authors of this RFC has experienced this while programming with Julia, which has two ways to declare functions.
We can mitigate this for Rust with clear guidelines, formatting rules, and Clippy lints, to steer users towards the canonical version in contexts where that is possible.
If formatted poorly, the block construct could "disappear" into the function signature. We recommend that the default Rust style use a newline to separate the type from the block, making this visually straightforward to parse.
The key strength of this proposal is that is a single concept that can subsume many additional features under consideration.
For example, we are considering adding gen { }
blocks that evaluate to
iterators. This means code like the following will be common:
fn countup(limit: usize) -> impl Iterator<Item = usize> {
gen {
for i in 0..limit {
yield i;
}
}
}
This immediately suggests adding a new gen fn
form, which would let us write
the same function as:
gen fn countup(limit: usize) -> usize {
for i in 0..limit {
yield i;
}
}
With this proposal, we could forego the new gen fn
form and instead write:
fn countup(limit: usize) -> impl Iterator<Item = usize>
gen {
for i in 0..limit {
yield i;
}
}
The story is similar for other new block types such as try { }
and
async gen { }
. For instance:
fn might_fail() -> Result<(), E>
try {
func()?;
another_func()?;
}
Another benefit is that it gives us a concise way to write what are effectively async functions where the returned future is named.
While async fn foo() -> i32
as a function that returns
impl Future<Output = i32>
is concise and convenient in many cases, there are
times when it is helpful to be able to name the type of the returned future.
This proposal would allow:
fn foo() -> NamedFutureType
async {
...
}
Another option would be to require a =
before the body. This could permit a
wider variety of expressions, such as fn inc(x: u32) = x + 1;
.
However, this would introduce more parsing complexity, for both the compiler
and the user. In particular, this would likely require a ;
for constructs
that don't end with a }
.
We could allow functions to have a top-level if
or if let
with an else
.
The compiler would not have problems parsing this. However, this would result
in having multiple braced blocks associated with a single function, which seems
more error-prone both for humans and for extremely simplistic code parsers
(e.g. those used within some code editors).
For simplicity, this proposal does not permit if
or if let
blocks that have
an else
. The compiler can recognize attempts to do this and offer a rustfix
suggestion to wrap the function body in braces.
- In Julia, functions can
be spelled
or
function f(x, y) x + y end
f(x, y) = x + y
. - C# and JavaScript have a
=>
form for defining functions.
We could introduce shorthand syntax for types that are produced by expression
blocks, such as async T
for impl Future<Output = T>
and gen T
for
impl Iterator<Item = T>
.
In addition to being useful in their own right, these types would work very well with this proposal, since the shorthand applies in the cases where we expect the function body block syntax to be used most often.
For example:
fn countup(limit: usize) -> gen usize
gen {
for x in 0..limit {
yield i;
}
}
fn do_something_asynchronously() -> async ()
async {
do_something().await;
}
Note that while this is an improvement, this results in the keyword (gen
or
async
) appearing twice. We may want to seek an alternative that allows
writing the keyword just once. However, we also want to preserve orthogonality
(type syntax having the same meaning everywhere, block constructs having the
same meaning everywhere), and avoid special cases.