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

[DRAFT] RFC: Function body shorthand syntax #17

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Centril
Copy link
Owner

@Centril Centril commented Nov 22, 2018

Rendered

This is a draft version of an RFC for you to review, before a formal proposal is made for consideration.

cc @nikomatsakis @alercah @eddyb @kennytm

Finally, a rather minor motivation, which still bears mentioning, is
that by permitting `fn foo(...) -> R = expr;` as a syntax, we bring `const`
and `static` items closer to `fn` items syntactically which gives a smoother
experience overall. In other words, we can now write:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that this gives a smoother experience. I think this is confusing, as static, consts and fns work differently. Having equality makes it seem like functions should be eagerly evaluated.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really the place to debate the proposal itself but...
I think that's quite a stretch; in none of the languages noted in the prior art does such confusion occur as there's clearly an argument that needs to be provided to the function before anything happens, so how could it possibly be eagerly evaluated?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the languages I would argue are not analogous to Rust (e.g. Haskell / any lazy language), but regardless, a function with no arguments could plausibly be eagerly evaluated. In many of the languages, = is the default way to specify a function, which I think makes it less confusing.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • A function with no arguments is really a function that is () -> ReturnType so it still takes an argument.
  • In a function with no arguments you are also more likely to write fn main() { ... } because the purpose of such a function is primarily its side effects.
  • Also, most functional programming languages, e.g. ML, OCaml, Scala, and Idris are all strict but no such confusion occurs.
  • Scala, Kotlin, and C# all support both { ... } and = (or => in C#) and there doesn't seem to arise such confusion from this. Notably the Scala users that use Rust seem to like this idea from what I can tell.

@varkor
Copy link

varkor commented Nov 22, 2018

I think this is a bad idea. You add extra syntax to the language, making it harder to learn ("What's the difference between a function defined with = as opposed to {}?"), without clear precedent in the language, for minimal benefit (as far as I see, just "ameliorating rightward drift"). It's not worth adding a whole new syntax to the language just to stop people having to indent an extra couple of times.

This makes the language less consistent, not more consistent. I think we should be trying to address the confusing/inconsistent parts of the language first before trying to add syntactic sugar wherever possible.

@Centril
Copy link
Owner Author

Centril commented Nov 22, 2018

Again this is not really the place do discuss the proposal itself; only to make factual corrections, typo fixes, add more context, or other sort of fixes...

You add extra syntax to the language, making it harder to learn

I don't think it makes it notably harder to learn; the meaning of the syntax is inferable from the syntax itself.

"What's the difference between a function defined with = as opposed to {}?"

What is the difference if you write let x = expr; or let x = { ... }? It's the same concept. The former permits a single expression, the latter permits statements and let bindings inside.

without clear precedent in the language

To me const and static items are precedent. Also note the large amount of precedent in other expression oriented languages.

minimal benefit (as far as I see, just "ameliorating rightward drift")

Rightward drift yes (which is not minimal imo), but also a) more familiarity for functional programmers, b) fully embracing the expression oriented feeling, c) encouraging smaller function definitions.

This makes the language less consistent, not more consistent.

I again disagree due to the aforementioned improvements wrt. expression oriented nature as well as const and static.

I think we should be trying to address the confusing/inconsistent parts of the language first

I've written several such proposals that plug consistency holes and I can do both at the same time. :)

before trying to add syntactic sugar wherever possible.

What are you referring to? In which way is this "wherever possible"?
I don't even see this as syntactic sugar to begin with.

@varkor
Copy link

varkor commented Nov 22, 2018

Again this is not really the place do discuss the proposal itself; only to make factual corrections, typo fixes, add more context, or other sort of fixes...

Okay, it'll be better to save my comments for when the RFC is posted, in that case. (Feel free to hide my posts as off-topic!)

@Centril
Copy link
Owner Author

Centril commented Nov 22, 2018

Again this is not really the place do discuss the proposal itself; only to make factual corrections, typo fixes, add more context, or other sort of fixes...

Okay, it'll be better to save my comments for when the RFC is posted, in that case. (Feel free to hide my posts as off-topic!)

Well; feel free to continue the discussion here with me since we already started it... :)
Maybe something fruitful can come of it?

@joshtriplett
Copy link

joshtriplett commented Nov 25, 2018

So, I'm torn. On the one hand, I can see some really compelling arguments for this, and I can easily imagine wanting to use it myself. On the other hand, I feel like this could easily let a braced construct get confused with function braces in a way that isn't possible today.

I feel like this would not be intuitive for anyone not coming from a language that had it. I don't really want function syntax to look like a simple assignment. I do also feel like @varkor has a point. This is a fundamentally different = than elsewhere in the language.

Can we go back to the problem you're trying to solve, and consider other ways to solve it?

@petrochenkov
Copy link

Not sure why I was pinged, but I actually like this despite my general conservativeness towards extending the language!

First, this is a very tiny and very surface change technically, not a full-blown new feature, but the effect is great.
Second, this closes the door for more special-cased proposals like match fn that I see from time to time.

But primarily, I like using one-liners when content does fit into a single line, and saving vertical space in general (and hate rustfmt that I suspect is a heir of Alexandre Dumas and is paid by the line).

For symmetry we can probably extend const/static items later to support braced initialization later

const C: u8 {
   /* long const-evaluation */
}

This would also emphasize the fact that const/static initializers and fn bodies are objects of the same nature.

@joshtriplett
Copy link

joshtriplett commented Nov 25, 2018 via email

@Centril
Copy link
Owner Author

Centril commented Nov 25, 2018

@joshtriplett

Can we go back to the problem you're trying to solve, and consider other ways to solve it?

The core goal is to make it really ergonomic to write short and descriptive function definitions (as opposed to long and deep / cyclomatically complex spaghetti) and to thus improve upon readability and in particular the maintainability of code bases.

So, I'm torn. On the one hand, I can see some really compelling arguments for this, and I can easily imagine wanting to use it myself.

I'm almost a bit embarrassed to be proposing a syntax thing which I would love to use myself so much instead of writing big and semantically significant proposals. =P

On the other hand, I feel like this could easily let a braced construct get confused with function braces in a way that isn't possible today.

Could you elaborate? The reverse syntactic unification that @petrochenkov noted should make that quite difficult I think. I thought about proposing it in this RFC but ultimately I thought I'd do one controversial step at a time. ;)

I feel like this would not be intuitive for anyone not coming from a language that had it. I don't really want function syntax to look like a simple assignment. I do also feel like @petrochenkov has a point. This is a fundamentally different = than elsewhere in the language.

Function definitions in the most basic mathematics classes are usually denoted with f(x) = 1 + 2 (at least in basic mathematics in Swedish high schools) so the equality notation should be even familiar for anyone who hasn't used any of Haskell (+ family), ML, Scala, Kotlin, C#. I think given that functions are fundamentally about things you run to get an output and that we have const FOO: $type = $expr; makes the meaning intuitive; I don't expect there to be confusion re. eagerness / laziness as @varkor suggested.

@joshtriplett
Copy link

joshtriplett commented Nov 26, 2018 via email

@Centril
Copy link
Owner Author

Centril commented Dec 21, 2018

Finally got some time to get back to this... :P

@joshtriplett

Does it not seem quite easy to miss the bit between the = and the { and just see that as a function body?

Maybe... If while something { is pushed too far to the right of the screen (e.g. outside of it or barely) then I think it could make you forget that you are in a while loop. But it seems to me that the source of the problem is not =, but rather that while was pushed too far to the right. I'd instead focus on having rustfmt use a heuristic based on the assumed number of characters on a line (100 by default). You could for example subtract 10 characters from the default for the = form.

I don't think this would be confusing for match since the body has => in it which makes it clear that this isn't a normal block.

You could also say that the source of the problem is the long function body and if you write:

fn long_function_name<T: SomeTrait>(some: Arguments, more: Args) -> Type {
    while something {
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
    }
}

then it seems to me that it is just as easy to forget about while something {. It could just as well be:

fn long_function_name<T: SomeTrait>(some: Arguments, more: Args) -> Type {
    let foo = {
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
        // stuff
    };
}

I think my concerns might completely evaporate if we decided (via style team and rustfmt) to format such things in a way that the difference stands out, such as:

It seems to me that the consistent way to format = expr if what precedes = does not fit the screen would be:

fn long_function_name<T: SomeTrait>(some: Arguments, more: Args) -> TypeThatIsReallyTooDamnLong
= while something {
    // long function body
    // more long function body
    // spanning more than a page of code
};

This is what happens with const, static, and type aliases today.

Or similar. Yes, I realize that removes a bit of the advantage of reducing rightward drift, but frankly a function long enough that you need to worry about rightward drift should perhaps not be using this in the first place.

There are plenty of functions which do or should immediately match or if let on their arguments and then they span a few lines (say <= 50, which fits within a single page). Those are functions are an important motivation for this RFC and I think it's desirable for them to be written as:

pub fn eval_expr(expr: &Expr) -> Option<u128> = match expr {
    Expr::Lit(expr) => eval_lit(expr),
    Expr::Binary(expr) => eval_binary(expr),
    Expr::Unary(expr) => eval_unary(expr),
    Expr::Paren(expr) => eval_expr(&expr.expr),
    Expr::Group(expr) => eval_expr(&expr.expr),
    _ => None,
};

To not reduce the advantage of this style (or = try { ... }), it seems better to me to focus on horizontal length rather than block forms.

@joshtriplett
Copy link

joshtriplett commented Dec 22, 2018 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants