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

RFC: Named arguments #2964

Closed
wants to merge 6 commits into from
Closed

RFC: Named arguments #2964

wants to merge 6 commits into from

Conversation

Aloso
Copy link

@Aloso Aloso commented Jul 21, 2020

This RFC adds named arguments to functions. Named arguments must be prefixed with a dot, e.g.

fn split(string: &str, .at: char, .limit: usize, .case_sensitive: bool) {}

split("hello world", .at = ' ', .limit = 2, .case_sensitive = true);

I want to thank everyone who participated in the internals discussion!

🖼 Rendered

goto summary, motivation, guide-level explanation, reference-level explanation, drawbacks, rationale and alternatives, prior art, unresolved questions, future possibilities

@burdges
Copy link

burdges commented Jul 21, 2020

There are far more important topics for the lang team right now.

I worry named arguments encourage less well thought out interfaces: If you find yourself wanting this, then you should normally explore more nuanced type usage instead, wrapper types might improve correctness, and chaining builder methods yields more powerful interfaces.

At a high level, we should "have fewer joints and keep them well oiled", so some joints we already oil include traits, proc macros, and receiver syntax aka self which in Rust replaces currying from functional languages. In each, we could discuss changes or tooling that better addresses the same underlying concerns, so I'll give several examples, only the first and last of which involve the lang team.

We might discuss whether #![feature(fn_traits)] could be stabilized because they imply a reasonable builder-like syntax for named arguments via proc macros ala https://internals.rust-lang.org/t/named-arguments-increase-readability-a-lot/12467/96

It clear such proc macros provide enormous DSL capability, either with or without #![feature(fn_traits)], so merely improving the tooling for writing such proc macros helps too.

We could ask if docs present builder pattern interfaces well enough ala https://internals.rust-lang.org/t/improve-docs-for-builder-patterns/12126/3

We could discuss patterns and/or proc macros that simplify defining associated structures together, like both a signed and unsigned message, or both a type and some builder, and linking contextual information.

We could give structs and enums some special super item, so we could create them with method syntax like value.TypeDecl.

enum Foo { .. }
enum Bar {
    A { super: Foo, x: u32, y: u32 },
    B { super: Foo, z: f64 },
    C { .. }
}
impl Bar {
    go(&self) { .. }
}

fn f() -> Foo { .. }

fn blagh(x: u32, y: u32) {
    f().Bar::A{ x, y }.go()
}

In this, x and y are puns, maybe passing contextual information, while the distinguished non-punned field super: Foo takes the value returned by f() passed by receiver. Invoking Bar::go need less boilerplate than named arguments, ala go(super: f(), x: x, y: x). And it's far more powerful obviously.

In short, we should not make methods more like structs by adding named arguments, but instead make types more like methods.

@Aloso
Copy link
Author

Aloso commented Jul 21, 2020

I worry named arguments encourage less well thought out interfaces: If you find yourself wanting this, then you should normally explore more nuanced type usage instead, wrapper types might improve correctness, and chaining builder methods yields more powerful interfaces.

The RFC explains why these "solutions" are unsatisfactory in the rationale and alternatives section.

We might discuss whether #![feature(fn_traits)] could be stabilized because they imply a reasonable builder-like syntax for named arguments via proc macros ala https://internals.rust-lang.org/t/named-arguments-increase-readability-a-lot/12467/96

First, this works for functions but not for methods. Secondly, this relies on the fn_traits and unboxed_closures nightly features, and there are currently no plans to stabilize either of them, AFAIK.

We could ask if docs present builder pattern interfaces well enough ala https://internals.rust-lang.org/t/improve-docs-for-builder-patterns/12126/3

I don't think the documentation for builder patterns is the main issue. Creating builder types requires a lot of boilerplate, hence programmers often try to avoid using the builder pattern, and usually apply it only in public functions. I think that private functions should be readable as well!

Also note that the builder pattern emulates arguments that are both named and optional, which is a different use case.

I also explained this in the RFC.

We could discuss patterns and/or proc macros that simplify defining associated structures together, like both a signed and unsigned message, or both a type and some builder, and linking contextual information.

I'm not sure what you're getting at, could you provide an example?

We could give structs and enums some special super item, so we could create them with method syntax like value.TypeDecl.

This has some of the same downsides of the builder pattern, while also introducing a big new language feature that seems controversial and unjustified to me.

I'd like to stress that none of your ideas is backwards compatible with existing APIs. The current proposal allows adding named arguments to existing crates, without breaking backwards compatibility. This is a very valuable property, which should not be ignored.

In short, we should not make methods more like structs by adding named arguments, but instead make types more like methods.

This claim is unsubstantiated. Could you explain what that is better?

@lebensterben
Copy link

Suppose named arguments are allowed, soon people will ask for arbitrary argument order and optional arguments.

The concerns over these features should also apply here.

@Aloso
Copy link
Author

Aloso commented Jul 21, 2020

No. In this thread, only this RFC should be discussed. If you want to discuss other features like optional arguments, please do so in an RFC that proposes optional arguments.

P.S. This is a straw man. Just because "people will ask for x" doesn't mean that we are obliged to implement x.

@burdges
Copy link

burdges commented Jul 21, 2020

We'd benefit far more from stabilizing #![feature(fn_traits)] than from adding named arguments, so that topic more warrants lang team time. There are far more important "real" topics than this, like specialization, existing soundness holes, and const generics too, but I only discussed topics that address the same underlying concerns.

We'd also benefit far more from reducing the boilerplate involved in the builder pattern, which sounds doable without involving the lang team via improving proc macros tooling.

In fact, I suspect the super field and its value.TypeDecl syntax would largely eliminate the boilerplate from the builder pattern. I've no idea if value.TypeDecl fits with our parser, but if so it might be a smaller rustc change, and maybe even a smaller language docs change, than named arguments. It's vastly more powerful too..

We cannot add arguments to existing methods without breaking backwards compatibility either.

No. In this thread, only this RFC should be discussed. If you want to discuss other features like optional arguments, please do so in an RFC that proposes optional arguments.

All features carry costs, so their relative costs compared with other features should always be a focus.

@Aloso
Copy link
Author

Aloso commented Jul 21, 2020

We'd benefit far more from stabilizing #![feature(fn_traits)] than from adding named arguments

I don't think that these are mutually exclusive. I also don't understand how stabilizing #![feature(fn_traits)] would solve the problems addressed by this RFC.

We cannot add arguments to existing methods without breaking backwards compatibility either.

Right, but we can add argument names backwards compatibly:

fn foo(arg: i32);
// can be changed to
fn foo(.arg: i32);

Which allows calling foo with a positional argument (foo(42)) and with a named argument (foo(.arg = 42)).

@PoignardAzur
Copy link

Being realistic

Instead of looking at how code could be written in carefully crafted APIs, we should look at how code is being written in reality. Programmers don't always have time to rack their brains over how to create the most beautiful API. They want to get things done.

Named arguments allow iterating quickly without sacrificing readability, because they are dead simple. There's no need to create new types or make up long function names.

I like this part. It should be written in huge burning letters.

@mersinvald
Copy link

mersinvald commented Jul 21, 2020

@PoignardAzur about this section: being realistic, how often do we need named, but non-optional and non-default parameters in the real world?

I might be missing something, but it seems that the only problem the proposed change solves is lack of parameter name hinting in functions with long parameter lists, and this is already solved by IDEs effectively.

Is there a plan to go forward with that syntax to also bring default and/or optional named parameters?
If so, I think the specifics should be discussed before accepting this RFC, so we wouldn't end up with a conflict in the future.

If named parameters aren't meant to be anything more than described in this RFC, though, I'm not sure the problem they solve even exists. (for anything except blogposts)

@lebensterben
Copy link

There are multiple issues not addressed in the current RFC.

  1. Though named argument are opt-in feature, as proposed by this RFC, any changes in the names would result in a breaking change. But if library developers don't change the name, to avoid making breaking changes, a bad name probably causes more confusions than no names at all. Therefore, the following statement doesn't hold:

    Programmers don't always have time to rack their brains over how to create the most beautiful API. They want to get things done.

    Because people still have to spend time devising good parameter names.

  2. This RFC disallow using argument name for positional argument in function calls, e.g.

    fn foo(a: i32, .b: i32, .c: i32) {}
    foo(.a = 1, .b = 2, .c = 3); // ERROR! 1st argument is not named

    This implies that now developers need memorize one more thing, that what arguments are positional and cannot be named in function calls, and what arguments are named.

  3. In terms of compilation. The function definition now takes one extra layer that it needs to determine whether an argument is named or not. This cost looks minimal.
    But as for function calls, the compiler would need to verify more stuff:

    • Named arguments are supplied in the same order as they are defined
    • Named arguments are not followed by positional arguments
      I'd like to know how would this impact the compile time.

@robinmoussu
Copy link

Though named argument are opt-in feature, as proposed by this RFC, any changes in the names would result in a breaking change. But if library developers don't change the name, to avoid making breaking changes, a bad name probably causes more confusions than no names at all.

This is exactly the same situation than a badly named function.

This RFC disallow using argument name for positional argument in function calls [...]. This implies that now developers need memorize one more thing, that what arguments are positional and cannot be named in function calls, and what arguments are named.

The goal isn't to make it easier to write, but easier to read. And nothing prevents library author to make all of their arguments named if at least one is named.

@rolandsteiner
Copy link

I am of the opinion that the overlap of functions that would really benefit from named arguments with functions that would really benefit from refactoring is very large.

The examples in the RFC are not really convincing IMHO. For example, the Windows::new function really should take a Rect argument as a second parameter. Then the caller could choose whether to use top-left-bottom-right, or top-left-width-height, rather than having that arbitrarily prescribed.

In short, I would really like to see real-world examples from a selection of different libraries or applications, rather than theory-crafting with fn foo.

@robinmoussu
Copy link

@PoignardAzur about this section: being realistic, how often do we need named, but non-optional and non-default parameters in the real world?

It is detailed in the RFC. It's to allow backward compatible migration between positional and named functions. I agree that if backward compatibility wasn't an issue, it could totally be possible to remove this possibility. As explained in the RFC this can be done using a clippy lint.

I might be missing something, but it seems that the only problem the proposed change solves is lack of parameter name hinting in functions with long parameter lists, and this is already solved by IDEs effectively (emphasis mine).

At repeated over and over, using an IDE should not be a requirement to be able to read Rust code. This is even [detailed](https://github.com/Aloso/rfcs/blob/named-arguments/text/0000-named-arguments.md#parameter-hints-in-ides] in the RFC. Blog posts, git diff, Github, URLO, IRLO, stackoverflow, … don't have IDE capabilities, and it shouldn't be harder to read Rust code using any of those tools than in an IDE.

Is there a plan to go forward with that syntax to also bring default and/or optional named parameters?

Please read the corresponding section in the RFC.

@robinmoussu
Copy link

In short, I would really like to see real-world examples from a selection of different libraries or applications, rather than theory-crafting with fn foo.

This a very valid question.

As an example, nearly every function in the proj crate would benefit from it. This is even emulated in the examples by using local variables (comments are mine):

 let from = "EPSG:2230";
 let to = "EPSG:26946";

// Without using local variables the following call would be ambiguous)
 let nad_ft_to_m = Proj::new_known_crs(&from, &to, None).unwrap();
 let result = nad_ft_to_m
     // This API is very error prone since sometime you use the natural ordering (latitude, longitude),
     // and sometime the normalized one (eastern, northern)
     .convert(Point::new(4760096.421921f64, 3744293.729449f64))
     .unwrap();

Rewriting it using named arguments make it unambiguous:

 let nad_ft_to_m = Proj::new_known_crs(.from="EPSG:2230", .to="EPSG:26946", .area=None).unwrap();
 let result = nad_ft_to_m
     .convert(.lat=4760096.421921f64, .lon=3744293.729449f64)
     .unwrap();

@Aloso
Copy link
Author

Aloso commented Jul 21, 2020

Another use case is this module, which calculates sunrise/sunset times depending on the latitude and longitude, using trigonometry. I already refactored it to use a struct for Julian days. But there are still many functions with a less than ideal API, e.g.

fn time_of_solar_elevation(century: f64, t_noon: f64, lat: f64, lon: f64, elev: f64)

Creating new types (e.g. struct Coordinate { lat: f64, lon: f64 }) would be the most idiomatic solution, but that would add a lot of boilerplate, and since most of the functions are private, it's not considered worth the effort.

@Aloso
Copy link
Author

Aloso commented Jul 23, 2020

A few more real-world use cases from rust-analyzer:

pub(crate) fn report_progress(
    &mut self,
    title: &str,
    state: Progress,
    message: Option<String>,
    percentage: Option<f64>,
) {...}

pub fn analysis_bench(
    verbosity: Verbosity,
    path: &Path,
    what: BenchWhat,
    memory_usage: bool,
    load_output_dirs: bool,
    with_proc_macro: bool,
) -> Result<()> {...}

pub fn iterate_method_candidates<T>(
    &self,
    db: &dyn HirDatabase,
    krate: Crate,
    traits_in_scope: &FxHashSet<TraitId>,
    name: Option<&Name>,
    mut callback: impl FnMut(&Ty, Function) -> Option<T>,
) -> Option<T> {...}

pub fn iterate_path_candidates<T>(
    &self,
    db: &dyn HirDatabase,
    krate: Crate,
    traits_in_scope: &FxHashSet<TraitId>,
    name: Option<&Name>,
    mut callback: impl FnMut(&Ty, AssocItem) -> Option<T>,
) -> Option<T> {...}

pub fn find_node_at_offset_with_descend<N: AstNode>(
    &self,
    node: &SyntaxNode,
    offset: TextSize,
) -> Option<N> {...}

fn adjust(
    db: &dyn HirDatabase,
    scopes: &ExprScopes,
    source_map: &BodySourceMap,
    expr_range: TextRange,
    file_id: HirFileId,
    offset: TextSize,
) -> Option<ScopeId> {...}

pub fn add_crate_root(
    &mut self,
    file_id: FileId,
    edition: Edition,
    display_name: Option<String>,
    cfg_options: CfgOptions,
    env: Env,
    proc_macro: Vec<(SmolStr, Arc<dyn ra_tt::TokenExpander>)>,
) -> CrateId {...}

pub fn add_dep(
    &mut self,
    from: CrateId,
    name: CrateName,
    to: CrateId,
) -> Result<(), CyclicDependenciesError> {...}

fn dfs_find(&self, target: CrateId, from: CrateId, visited: &mut FxHashSet<CrateId>) -> bool {...}

These are just the most obvious functions that would benefit from named arguments, which I found by skimming through parts of the workspace.

@stevenblenkinsop
Copy link

stevenblenkinsop commented Jul 24, 2020

As proposed, this wouldn't allow argument labels to be added to trait methods backwards compatibly. The requirement that labels in trait implementations must match the labels in the trait definition means that adding a label in the trait definition invalidates existing implementations. I think, in order to be consistent with the motivation for this design, labels in implementations should be optional, but if present, must match the corresponding label in the trait definition, and must follow the rule that positional parameters cannot follow named parameters. The labels defined in the trait definition can be used at the call site irrespective of whether they appear in the implementation for a particular type.

@Aloso
Copy link
Author

Aloso commented Jul 24, 2020

@stevenblenkinsop I think this is a good idea. Given this example:

trait Trait {
    fn foo(&self, .arg: i32); // named arg
}
struct Struct;

impl Trait for Struct {
    fn foo(&self, arg: i32) {} // positional arg
}

Then the following function calls are equivalent:

Struct.foo(42);
Trait::foo(&Struct, 42);
<Struct as Trait>::foo(&Struct, 42);

So it should be allowed to use named arguments in all cases:

Struct.foo(.arg = 42);
Trait::foo(&Struct, .arg = 42);
<Struct as Trait>::foo(&Struct, .arg = 42);

My only concern is, does it make sense that Struct.foo(.arg = 42) can be called with a named argument, even though the implementation uses a positional argument? This might cause confusion, but I guess that compatibility concerns outweigh this minor inconsistency.

(After all, the argument patterns can differ between the trait definition and the implementation, too).

@amosonn
Copy link

amosonn commented Jul 24, 2020

Since this is only syntactic sugar, you could argue for Struct.foo(.arg = 42); not working,
Trait::foo(&Struct, .arg = 42); working, and bikeshedding on whether <Struct as Trait>::foo(&Struct, .arg = 42); works (I would say yes).

However, the main reason for forbidding the named use of positional arguments is so that naming an argument (adding semver guarantees) is an opt-in choice; in this case, the API is owned by the trait definition, not by its implementation for the struct, and the trait owner already opted-in to named arguments - so there's no reason to avoid them.

Copy link

@matu3ba matu3ba left a comment

Choose a reason for hiding this comment

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

Could you provide feedback on my comments?

text/0000-named-arguments.md Outdated Show resolved Hide resolved
text/0000-named-arguments.md Show resolved Hide resolved

foo(.a = 1, .b = 2, .c = 3); // ERROR! 1st argument is not named
foo( 1, .b = 2, .c = 3); // ok
foo( 1, 2, .c = 3); // ok
Copy link

Choose a reason for hiding this comment

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

I would prefer all or nothing, so no mixed cases, if possible.

Choose a reason for hiding this comment

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

Can this flexibility at the call site be motivated? The motivation for names being optional at the call site is so that names can be added to existing APIs. This would be served by an all or nothing requirement at the call site, however. Is the idea that an API might initially not define a name for the first argument, but later decide that it wants one? Or that APIs might define a name for the first argument where one isn't needed, so the call sites should have the option to leave it off? I'm not sure allowing these cases warrants having quite so many different ways to call the same function.

Copy link

@amosonn amosonn Jul 25, 2020

Choose a reason for hiding this comment

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

Otherwise we have a situation, where making a positional argument named in a function that has no named arguments is not a breaking change, because nobody could be calling it with named arguments yet; but making an argument named in a function that already has some names is breaking, because you might have users calling using those names, and those calls will become calls with partial use names. It is true that the first case is important so that introducing this feature doesn't break APIs, and the second one isn't; but still this seems like a strange inconsistency.

IMHO, removing call-site flexibility is only tenable if the function definition is required to have all-or-none (in which case, once you add names, you can't add any more names).

Copy link
Author

Choose a reason for hiding this comment

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

Actually, the reason was that I wanted to allow specifying only some of the argument names. However, I think that positional args after named arguments should be forbidden, to be forwards compatible with optional args and named args in arbitrary order.

Currently, this means that named arguments can be added backwards compatible from right to left, although that is not the primary motivation:

fn foo(a: i32, b: i32, c: i32);
fn foo(a: i32, b: i32, .c: i32);
fn foo(a: i32, .b: i32, .c: i32);
fn foo(.a: i32, .b: i32, .c: i32);

If optional args are added in a future RFC, this will be much more flexible. So when omitting an optional argument, arguments after the omitted one must be named, but arguments before the omitted one can be positional.

Choose a reason for hiding this comment

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

When I commented that we need take into consideration the concerns over optional arguments and arbitrary order you said that was a straw man.

Then what's this?

Copy link
Author

@Aloso Aloso Jul 25, 2020

Choose a reason for hiding this comment

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

I want to make sure that optional arguments will be compatible with this RFC, if they are added in the future. However, there is no guarantee that they will be added even if this RFC is accepted. That's why I said that we should concentrate on the features proposed in this RFC, and not start a discussion about benefits and downsides of optional arguments, since that can derail the discussion.

Copy link

@amosonn amosonn Jul 25, 2020

Choose a reason for hiding this comment

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

I think the strawman was referring to this framing:

Suppose named arguments are allowed, soon people will ask for

which is actually Slippery Slope (but still not a relevant argument). Forward compatibility with those features should be guaranteed, but the merit of this RFC should be judged on its own.

Copy link

Choose a reason for hiding this comment

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

Linking the intended behavior by the lint should be fine here.

text/0000-named-arguments.md Outdated Show resolved Hide resolved
text/0000-named-arguments.md Outdated Show resolved Hide resolved
text/0000-named-arguments.md Show resolved Hide resolved
@Aloso
Copy link
Author

Aloso commented Jul 25, 2020

@stevenblenkinsop I added your idea to make argument names optional not only in function calls, but also in trait implementations.

Copy link

@matu3ba matu3ba left a comment

Choose a reason for hiding this comment

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

Reason for not reordering variables (Rust does currently not support this? and the authors opinion) is missing.
The planned lint behavior (default behavior/changeable behavior?) should be worded/explained more specific.

text/0000-named-arguments.md Show resolved Hide resolved
text/0000-named-arguments.md Outdated Show resolved Hide resolved
text/0000-named-arguments.md Show resolved Hide resolved
text/0000-named-arguments.md Show resolved Hide resolved
Comment on lines +295 to +306
However, it should be allowed to omit the argument name, when it matches the variable, field or call expression that is passed as the argument, for example:

```rust
fn foo(.arg1: i32, .arg2: i32, .arg3: i32) {}

let arg1 = 42;
let s = Struct { arg2: 42 };
fn arg3() -> i32 { 42 }

foo(.arg1 = arg1, .arg2 = s.arg2, .arg3 = arg3()) // no warning
foo(arg1, s.arg2, arg3()) // no warning
```
Copy link

Choose a reason for hiding this comment

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

This should belong to the function section. I am not sure, if it is very smart to add work to the compiler to check this.

Copy link
Author

Choose a reason for hiding this comment

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

It's a lint, so it doesn't add work for the compiler, only to cargo clippy.

@mattiasdrp
Copy link

mattiasdrp commented Jul 27, 2020

Hey, thanks for doing this RFC!

Not sure about the dots that are already used a lot but well, they are easy to write (I have a preference for # or ~ for example).

I'm actually so glad there's a RFC for this and that it focuses on just that and not optional arguments that are indeed usually closely linked to named arguments but allow to too many digressions that corrupt the discussion.

@jgarvin
Copy link

jgarvin commented Jul 31, 2020

Keyword arguments require every arguments to be specified at the call expression similar to how structs need every field value to be specified at the initialization expression.

Which brings up a great point -- if named parameters are so terrible why are they good for struct constructors? What argument against name parameters doesn't also apply to struct constructors? The syntax for struct constructors I think is one of the things that makes the language more clear, I just think you should be able to use that syntax for more functions.

The point that you are forced to specify all of them is only true so long as we also don't have default arguments. That's a separate issue but comparing against struct constructors is again informative -- the lack of ability to specify defaults for struct fields leads people to resort to ..T::default() which again will have worse compile time performance and has efficiency that questionably depends on the optimizer to recognize many of the fields of the temporary won't be read and hopefully avoid extra heap allocations.

@robinmoussu
Copy link

robinmoussu commented Jul 31, 2020

So what is the reason why named arguments need to be explicitly called out in the function declaration?

@wiogit Please read the RFC. Why make named arguments opt-in?

@wiogit
Copy link

wiogit commented Aug 1, 2020

Which brings up a great point -- if named parameters are so terrible why are they good for struct constructors? What argument against name parameters doesn't also apply to struct constructors?

For me it comes down to convention over configuration. A struct is purely comprised of named fields or purely comprised of positional fields for tuple structs. The name/position design is chosen for the entire struct, not for each individual field. Yes, there is added clarity by having structs constructors to use named fields, but it is also the only way it could be done while keeping the ordering of fields as an implementation detail.

What is your opinion of the following code?

struct MyStruct(String, .x: f32, .y: f32);
let my_struct = MyStruct("East City".to_owned(), .x = 25., y= 0.);

let my_tuple: (String, .x: f32, .y: f32) = ("North City".to_owned(), .x = -40., .y = 0.);

enum MyEnum(u32, .Decimal: f32, .Undefined);
let my_enum1 = MyEnum::0(40);
let my_enum2 = MyEnum::Decimal(40.);
let my_enum3 = MyEnum::Undefined;

@Ichoran
Copy link

Ichoran commented Aug 1, 2020

@wiogit - Except for the dots in front of the variable names, which are awful (unnecessary, and bring to mind unrelated things), the struct looks fine. There's no point having partially-named tuples and structs, so why bother with the second? And the dots are still ugly / distracting / pointless.

The enum is awful--the whole point of enums is to be able to talk about the alternatives, which means they need names!

Anyway, I don't see how this is related to functions getting external argument names. Functions already have argument names. The thing is that the names aren't part of the public API, and the question is whether that should be possible.

It's a different issue than whether we should be able to elide argument names entirely.

This does point out a weird aspect of function argument lists. From the inside, they look identical to regular structs:

struct Coord{ x: f64, y: f64 }

let c = Coord{ x: 2, y: -2.2 };
// can talk about c.x and c.y

fn foo(x: f64, y: f64) {
  // Can talk about x and y; 'c.' is implicit
}

But from the outside they look identical to tuples!

let x = 2.7;
let y = 0.14;
let v = (x, y);
foo(x, y);

This is...kind of weird. But we all accept it without blinking an eye.

Furthermore, why the asymmetry between arguments and return values? Mathematically, a function is just a map between two sets.

fn swap(x: f64, y: f64) -> (f64, f64) { (y, x) }

The return value is unabashedly a tuple, which is the canonical un-named form of tuple structs, which is a struct. You know this because you can

let v = swap(1, -1);

You're not required to take apart the pieces (though you can if you want to).

So, what's the deal? Function outputs are exactly tuples, i.e. structs. If this is cool with outputs, why isn't it cool with inputs? Why isn't it cool to at least adapt it to look that way, even if the implementation may differ; or to allow functions for which it is true even if it isn't true normally.

My point is that there are a lot of sensible ways to think about things, and if Rust shifts a bit from one way to another, as long as it doesn't end up incoherent, I don't think we should oppose it reflexively as being "not Rust". Some things aren't Rust, and we should identify those and keep them out. But things that are consistent with what we've already got should be considered as potential changes/additions. (Obviously, subtractions are a problem for backward compatibility...and if you can only add and not subtract you should have a high bar for acceptance since you can't fix your mistake.)

So this is why I think the let's-tack-on-an-ad-hoc-syntax-for-functions-that-we-already-solved-in-a-different-way-for-other-parts-of-the-langauage approach is a bad one. It's pure complexity added to the language, because it doesn't accomplish its goal by reimagining what is sensible and making a new sensible whole. It's just new rules, coming out of nowhere, that you couldn't possibly have guessed from learning about other parts of the language.

(And notationally, tacking on symbols to things makes code feel less elegant, which is another downside. But even if it was fn foo(pub x: f64, pub y: f64) -> { x - y } I would still object that it's an ad-hoc add-on to have a second way to solve an already-solved problem. But at least by reusing the pub keyword, one could sorta guess what it might mean. The dots are entirely inscrutable.)

@oblitum
Copy link

oblitum commented Aug 1, 2020

@Ichoran do you have confidence that Rust is open to reevaluate the meaning of all its data structures to make them mutually compatible, instead of having an incremental addition? For me this would be a miracle in language evolution, I've never seen a language refactor its underpinnings after the ship has sailed for too long.

@aloucks
Copy link

aloucks commented Aug 1, 2020

I think a much simpler alternative would be to allow anonymous struct declarations in parameter positions. This would be conceptually like a struct-name version of lifetime elision.

struct FooArgs {
  x: i32,
  y: &'static str,
}

fn foo(f: FooArgs) {}

foo({ x: 1, y: "hello"});

@oblitum
Copy link

oblitum commented Aug 1, 2020

@Ichoran
Copy link

Ichoran commented Aug 1, 2020

@oblitum - The incremental addition would be to add a little syntactic sugar to make it easier to utilize a similarity that's already there.

I don't know why you feel that this is a dramatic, daring step in language design. It's no more dramatic conceptually than field init shorthand.

@joshtriplett
Copy link
Member

One quick note: this doesn't require full structural records. We've talked about having type inference, such that func(_ { a: value, b: othervalue }) would construct the appropriate structure expected by func. That doesn't require redesigning structs, just adding one bit of additional type inference.

@BurntSushi
Copy link
Member

BurntSushi commented Aug 2, 2020

Moderation note: As some of you may know, RFC discussions can be particularly straining on folks, especially when there are a lot of comments in a short period of time. There is even some discussion ongoing on how best to modify the process to make it easier on everyone involved. Until that time though, I'd like to remind everyone to be their best selves so that we can move forward collaboratively. In light of that, I'd like to urge everyone to think about these things when responding:

  • Does your comment meaningfully advance the discussion forward? If not, reconsider posting it, to avoid adding to the already long list of comments.
  • Do your best to put yourself in the shoes of the person you're responding to. Even if you didn't mean it, do you think your comment might come across as antagonistic to them? If so, consider re-wording it. When discussing in a pure text medium with a bunch of strangers, we don't have the luxury of relying on rapport, body language or facial expressions to convey the sense of our words.
  • The main goal of RFCs and, indeed, participating in the Rust community, is for us to collectively move toward to an ideal Rust. Consider this goal when responding and ask yourself whether your comment helps move us in that direction.
  • Have you already posted a comment less than an hour ago? If so, consider sitting on your thoughts a bit before posting again.

Thank you all for listening!

@LionsAd
Copy link

LionsAd commented Aug 2, 2020

I think any feature added should be just syntactical sugar for a more verbose syntax.

This is hence not preventing a later .a = 1 syntax, but rather outlining a road to it:

Given I have a:

fn add(i32, i32) -> i32

The first thing is I want to be able to create a struct, so that I can do without macro usage:

type addType = add.params; // magic struct?

add(AddType {a: 1, b: 2}.into())

The problem one is that right now it’s not possible to even create the arguments of a function.

This already gives a lot of trouble for generator parameters or creating a generic wrapper for async fns or closures.

Any fn defined MUST BE where F: Fn(int32, int32)

It’s not possible to define a generic callback fn type - the compiler can however derive the type automatically.

However as has been expressed already, the argument to a function is essentially a named tuple.

Here we go back into the isomorphism territory and I agree that conceptually:

Color(a, b, c)

Is not the exact same semantically as:

Color {}

However storage and internal representation wise they are memory layout very similar IIRC.

So we can say that functions essentially right now take an anonymous tuple struct.

So a fn add(i32, i32) is in reality of type:

fn add__type((i32, i32))

So every function in Rust essentially takes a struct tuple as Parameter.

Now once we are this far that we can call a fn differently using a tuple, it is clear that we can convert from a struct into this named tuple.

And now that we are here we can also just use [#derive(NamedArguments)]

to make it all work as now we should be able to define an Autotrait NamedArguments that allows to use a named struct in addition to what is essentially a tuple struct.

And then once we have that, we can discuss again if we want to automatically desugar:

(.a = 1, .b = 2)

to

({a: 1, b: 2})

but syntactic sugar wise this is the last step, not the first one.

There must also be a way to programmatically create arguments tuples and argument structs for this syntactic sugar to work even however.

So my vote would be to postpone on implementing tuples, from conversions, etc. and just then discuss if we want to have .a = 1 Notation or not.

That also decreases workload on rust core team.

@amosonn
Copy link

amosonn commented Aug 2, 2020

Many comments here discuss the syntax at the call-site. While this is also an important consideration, the main hurdle for named arguments, which is addressed by the RFC and not addressed by (edit: some of) these comments, is the syntax at the definition site: how to mark named arguments as opt-in, possibly granularly, so that neither implementing this RFC nor changing the name of an argument aren't breaking changes.

Such syntax cannot be struct-like, as the only parallel from structs is marking a field as pub, but this does quite other things (in addition to making the field's name part of the public API): in particular, a struct constructor cannot be publicly called unless all of its fields are pub. Using this syntax here would be quite confusing; function arguments are always "public" in this sense, just their names (sometimes) aren't. (On an aside, to me this demonstrates that struct constructors and function calls are not that similar; the reason here is that the first is usually a private implementation detail, while the second often public API).

So one question that arises (and was discussed only briefly) is whether partial opt-in (mix of named and positional arguments) is necessary or not. If it isn't, maybe it could still be possible to model this as a switch selecting whether a function's arguments are a tuple or a struct. It seems that at least for reciever syntax a mixed mode is required, this could be perhaps special-cased to have self as a field of this struct, though this seems ugly to me. If partial opt-in is required, then a more complex mixture of tuple and struct is required, one which doesn't exist yet in Rust, and adding it just for function calls seems excessive IMHO. (Compound ideas like a tuple with a struct as the last argument are just workarounds).

Another concern is how to match the syntax at the definition-site and the call-site. To me it seems much more important that these two match up than the call-site syntax matching struct constructor. So if some novel syntax needs to be added to the function definition, it might as well be used in the call-site as well.

@LionsAd
Copy link

LionsAd commented Aug 3, 2020

First of all: I am am proponent of named arguments and like your syntax proposal. I am fairly new to rust, and that was like my first feature request ;) that I googled.

I am trying to find a way forward for rust itself, so that it's easy to implement the syntax sugar on top of something complex manual, but working.

Many comments here discuss the syntax at the call-site. While this is also an important consideration, the main hurdle for named arguments, which is addressed by the RFC and not addressed by (edit: some of) these comments, is the syntax at the definition site: how to mark named arguments as opt-in, possibly granularly, so that neither implementing this RFC nor changing the name of an argument aren't breaking changes.

That is indeed true, a macro would automatically make all parameters named, which is obviously not wanted.

Such syntax cannot be struct-like, as the only parallel from structs is marking a field as pub, but this does quite other things (in addition to making the field's name part of the public API): in particular, a struct constructor cannot be publicly called unless all of its fields are pub. Using this syntax here would be quite confusing; function arguments are always "public" in this sense, just their names (sometimes) aren't. (On an aside, to me this demonstrates that struct constructors and function calls are not that similar; the reason here is that the first is usually a private implementation detail, while the second often public API).

That is not really true, structs can have named and positional parameters already, also all fields are public by default, so you can write:

Sorry - I realized, that mixing is not possible, I think it should be though ... So consider that a hypothetical example.

struct Params {
  int32,
  int32,
  named_1: int32,
}

struct ParamsTuple(int32, int32, int32); // Implement Params -> ParamsTuple :)

let params = Params {0,1, named_1: 42};

and then access them via params.0, params.1, params.named_1.

I think that this would match pretty close to positional parameters + named parameters, further showing that there is some similarities between parameters and structs.

func(ParamsTuple(0, 1, 42));

func(Params {0, 1, named_1: 42});

The problem is that function arguments in rust are not a first-class citizen right now, which makes that rather difficult to do this at the moment.

I was not able to find the history why the magic rust-call was added to define the Fn type, where it's pretty clear that the definition should take a FnArgs trait, which then can be converted between types.

Another concern is how to match the syntax at the definition-site and the call-site. To me it seems much more important that these two match up than the call-site syntax matching struct constructor. So if some novel syntax needs to be added to the function definition, it might as well be used in the call-site as well.

Agree.

@scottmcm scottmcm added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 9, 2020
@joshtriplett
Copy link
Member

joshtriplett commented Aug 10, 2020

We discussed this in the @rust-lang/lang triage meeting today. Here's the consensus from that meeting:

We're not entirely opposed to the concept of better argument handling, and we've followed several of the proposals regarding things like structural types, anonymous types, better ways to do builders, and various other approaches.

However, we feel that this absolutely needs to be discussed in terms of the problem space, not by starting with a specific solution proposal that solves one preliminary part of the problem. And while we don't want to let the perfect be the enemy of the good, we do think that taking an incremental step requires knowing what the roadmap looks like towards the eventual full solution.

Between that and our roadmap this year being more about finishing in-progress things rather than taking on major new features, we're going to close this RFC. When we're ready to work on this, we'd want to see a proposal in the form of an MCP; however, we're not looking for that MCP this year.

@joshtriplett
Copy link
Member

@rfcbot close

@rfcbot
Copy link
Collaborator

rfcbot commented Aug 10, 2020

Team member @joshtriplett has proposed to close this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-close This RFC is in PFCP or FCP with a disposition to close it. labels Aug 10, 2020
@rfcbot
Copy link
Collaborator

rfcbot commented Aug 27, 2020

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Aug 27, 2020
@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Sep 6, 2020
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 6, 2020

The final comment period, with a disposition to close, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC is now closed.

@rfcbot rfcbot added to-announce closed This FCP has been closed (as opposed to postponed) and removed disposition-close This RFC is in PFCP or FCP with a disposition to close it. labels Sep 6, 2020
@rfcbot rfcbot closed this Sep 6, 2020
@adams85 adams85 mentioned this pull request Jun 12, 2024
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed This FCP has been closed (as opposed to postponed) finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.