Simplified syntax for simple functions

We can have simple function definitions for simple functions like below

fn multiply_by_2(n: i32) -> i32 {
    n * 2
} 

fn zeroes_array<const N: usize>() -> [i32; N] {
    [0; N]
}

The above functions can be simplified as below

fn multiply_by_2(n: i32) -> n*2;

fn zeroes_array<const N: usize>() -> [0; N];

we can use => for -> or anything else.

Define "simple", please.

Can I put a block after the -> and write any arbitrary function?

What's the motivation? Are you just wanting to not write the type, or are you trying to not write the {}s, are you wishing rustfmt put them on one line, or …?

9 Likes

It's probably a nonstarter. Obligatory link:

Sometimes I will use similarly trivial functions within the body of a function, closure syntax allows for a lot of abbreviation.

let multiply_by_2 = |x| x * 2;
14 Likes
  1. Block should be optional.
  2. return type should be optional, if it can infer.
  3. Yes, if it is like one expression or so, might as well format in one line.

Responding to your second type, an explicit return type is a written contract of the values that the function will return. Inferring return types opens the door to a lot of complexity

14 Likes

That same link does say:

That said, we are open to proposals that involve new syntax

And personally, I do hope that we add a syntax like this someday, for many different reasons but especially because it would provide a great way to write things like fn func(...) -> Result<T> = try { ... } without having to introduce more special syntax for such functions.

I expect that it'll take some long conversations and careful consideration, but it certainly hasn't been ruled out.

Agreed, but there's one circumstance where inferring return types would not break this property: when the body of the function already names the type at the top level, and we're doing exclusively local reasoning of propagating that named return type to the function signature (and documenting it in the rustdoc for that function).

For instance, if you have fn new(x: u64) -> Self { Self { x } }, the return type is already named in the body, and it'd be perfectly reasonable to write fn new(x: u64) = Self { x }.

On the other hand, I don't think it's reasonable to infer a return type if you write fn func() = otherfunc(), because that would rely on non-local inference, and because that would not allow type inference to stop at function bodies (making it less scalable, both for the compiler and for humans reading the code).

22 Likes

If we're making a simplified syntax for putting functions on one line, I'd personally get more use out of something like this for making trait implementations easier:

pub struct Foo { .. }
impl Foo {
    pub const fn new() -> Self { .. }
}

impl Default for Foo {
    // Declares that `Foo::default()` has the same parameters, return type,
    // and body as `Foo::new`.
    // 
    // Equivalent to `fn default() -> Self { Self::new() }`.
    fn default = Self::new;
}

I write this exact setup often because I want types that can be constructed in a const context, but also implement Default, and this is the best way to ensure that both options exist and are in-sync with each other.

As another upside, this avoids having to deal with type inference, since it can just copy the return type from the referenced function.

18 Likes

makes sense. thanks for giving more data points :slight_smile:

That syntax already exists! JavaScript users use its equivalent to define one line functions instead of 3 line function declaration too.

#[allow(non_upper_case_globals)]
const multiply_by_2: fn(i32) -> i32 = |n| n * 2;

One downside is, this function cannot be generic. Generic conjures could've been very helpful overall to be honest.

2 Likes

TBH, I think the other way around is better:

fn new(x: u64) -> Self { .{x} }

Type inference and other kinds of elision are easy wins in the body, but removing it from signatures is less obvious.

3 Likes

That doesn't necessarily work in all cases:

fn returns_future() = async { 42u64 }
fn returns_iter() = gen { yield 42u64 }
fn returns_option() = Some(42u64)
use eyre::Ok;
fn returns_result() = Ok(Self(42u64))

There are Expression-bodied members in C# and I find them really nice. It is one of those small features that are not essential but very pleasant to use. Like ? operator.

fn from(inner: u8) => Self(inner)

instead of

fn from(inner: u8) -> Self {
  Self(inner)
}

Or

pub fn descr(&self) -> format!(
    "Some very long description ({}): {}"
    self.xyz
        .map(|val| some_mapping(val + 1))
        .unwrap_or_default(),
    self.abc,
);
2 Likes

This syntax is technically tricky to parse — it puts types and expressions in the same position, so it requires parsing ahead to find ; or { after the type-or-expression, and then backtrack and re-parse it as the correct item. Rust had to give up on type ascription syntax, because distinguishing between types and expressions is tricky.

There have been proposals to use = before the body, which would be much easier to parse.

But I don't think that's a change worthy of making. Rust supports closures which already take care of majority of trivial functions. Rust also supports implicit return of the last expression in a block, which makes 1-liner functions a bit smaller too. There isn't much boilerplate left to remove, and IMHO it's not worth complicating the syntax to save a couple of chars in a rare case.

6 Likes

Agreed.

I think there are some very common cases where it arises.

Constructors:

fn new(many, params) -> Self {
    Self { many, params, thing: func(params) }
}

fn new(many, params) = Self { many, params, thing: func(params) }

Simple trait implementations:

impl Default for Thing {
    fn default() -> Self {
        Self::new()
    }
}

impl Default for Thing {
    fn default() = Self::new()
}

Functions using async, try, gen, or some combination thereof at the top level:

fn func() -> impl Iterator<Item = u64> {
    gen {
        yield 42;
    }
}

fn func() = gen {
    yield 42u64;
}

fn func2() -> eyre::Result<u64> {
    Ok(42)
}

fn func2() = eyre::Ok(42u64)
6 Likes

Potentially off-topic, but I find the "Read-only properties" C# has more interesting, more fitting for such simple functions and more useful for Rust in general (as they're more flexible), especially when combined with traits.

I assume most of these short functions we're talking about exist on a struct and not standalone.

Consider the following:

// Perhaps even allowing to derive traits that only contain fields that match
// #[derive(MyAbstraction)]
pub struct MyStruct {
    my_private_field: usize,
    pub my_public_field: usize,
    // Could potentially even allow making them read/get-only:
    // pub(super=get) my_public_field: usize,
}

impl MyStruct {
    // Note: Implicit &self/&mut/self self may not be easy/possible/make sense
    pub get name_of_old_field = self.my_private_field * 2;
    pub set name_of_old_field(value: usize) = {self.my_private_field = value/2};
}

pub trait MyAbstraction {
    // Associated types (exist already)
    // type Something;
    
    // Functions/Methods (exist already)
    // fn do_something(&self) -> usize;

    // Field accesses (implemented like functions, but not requiring brackets)
    // Not sure what a good syntax would be, may need to be like
    // fn (&self argument), but there may need to be a difference to functions due
    // to Rust allowing functions with the same name as fields.
    get my_public_field(&self): usize;
    get extra_field(&self): String;
}

impl MyAbstraction for MyStruct {
    // No need to list my_public_field if a corresponding field exists already.

    get extra_field(&self) = "Hello World";
}

fn run(s: impl MyAbstraction) {
    // No brackets needed
    let x: String = s.extra_field.toString();

    // Currently not possible through a trait unless this field is added
    // through a function.
    let y: usize = s.my_public_field;
}
fn run2(s: MyStruct) {
    s.name_of_old_field = 6;
    dbg!(s.name_of_old_field);
}

This would allow the following:

  • Implementations could change their memory layout (even if they have public fields), or even make formerly public fields no longer exist without a breaking change.
  • Can indicate that a field is easy/cheap to access, currently as_* methods are used for that.
  • Generics and impl Trait can access fields and not just methods/functions.

As far as I can tell that would also cover most situations where you'd want shorter function implementations, with the added benefit of not requiring brackets when they're called (which could be useful to indicate they are cheap to call.

One problem is of course that it can hide complexity and overhead from executing the getter, another that it could start a war over when to use functions and when not.

Though there are probably a variety of reasons I'm not thinking of why code that is executed should be called with brackets (besides the naming collision between fields and functions),

There's much more helpful in C#, where it saves the return.

Even there, though, I'm often torn whether it's worth it because it adds another meaningless choice when writing code, and another place where when you make something slightly more complex you have to then redo the syntax to a different form.

Also C#'s => doesn't change the type annotation requirements. So the equivalent in Rust to C#'s

int foo() => 4;

would be just

fn foo() -> i32 => 4;

which is exactly the same length as

fn foo() -> i32 { 4 }

anyway.

So I've always been skeptical of something like expression-bodied members in Rust.

In particular, are they worth bothering with over just using more "shortness"/"simplicity" heuristics in rustfmt? We didn't need "expression-bodied if blocks" to put short ifs on one line...

5 Likes

I think the cases you listed are different problems that would benefit more from a dedicated syntax.

With constructors, writing {} is the least of the problems. The big pain is in repeating most fields 3 times — in struct definitions, in argument definitions, and in the struct literal. This proposal doesn't meaningfully change the boilerplate. For constructors I'd prefer something removing the field name and type repetition.

With trait implementations, there's a big jump in boilerplate when you switch from derive to a custom implementation. We've got #[default] for that reason, and such annotations could be extended to some hypothetical #[default = Self::new]. It's also painful to exclude a field from Ord or Debug, and again this proposal only removes two characters from multiple lines of boilerplate.

4 Likes

I won't argue with that; derive(Constructor) would be lovely for simple cases, for instance.

I still think there's substantial value for some of the cases, though.

1 Like

I think it would have more limited value if we don't allow omitting redundant return values, yes.

You definitely made a lot of valid points. The feature is generally a minor syntax sugar and without return type inference, it becomes quite pointless.

1 Like