[Pre-RFC] named arguments


#1

This is my first attempt at an RFC, any feedback is welcome :slight_smile:

Keyword arguments have been proposed multiple times in multiple forms in the past and had been postponed for after the 1.0 release. 1.0 has been released and things have settled a bit, so I thought it would be a good time to jump-start the discussions more actively again.

Let me know if some parts need more clarification / details.

Relevant discussions


  • Feature Name: named_arguments
  • Start Date: 2016-08-07
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

This RFC adds named arguments to Rust. It focuses only on named arguments and not on default arguments or variable-arity which are more complex to implement and delicate to get right.

Motivation

Rust strives to be maintainable for large code bases. named arguments would allow some function calls to be more explicit and thus easier to read and write.

Let’s consider the following fictive examples:

http.get("https://www.rust-lang.org/en-US/", None);

Window::new("Title", 20, 50, 500, 250);

free_fall(100.0, 0.0, 9.81);

Those are all functions / methods that I could see written in Rust. When calling those functions you have to remember the order in which the arguments appear. And at first sight, you can not really tell what the arguments stand for.

Now let’s consider calling the same functions with named arguments:

http.get("https://www.rust-lang.org/en-US/", timeout: None);

Window::new(title: "Title", x: 20, y: 50, width: 500, height: 250);

free_fall(z0: 100.0, v0: 0.0, g: 9.81);

It is now very clear what the arguments stand for, which makes maintaining such code a little easier.

Detailed design

The main goals of this RFC are:

  1. Make it backwards compatible
  2. Make it future proof
  3. Give API authors the ability to choose if the name of the arguments are part of the API or not

API authors are in control

Let’s start with the last point. Many proposals to add named arguments to Rust assumed that all arguments would become optional named arguments. However, this would make all the argument names part of the public API and renaming them would become a breaking change.

In this proposal I wish to give the ability to the API authors to choose if they want to commit to the argument names by making them part of the public API.

I propose to do this with the following syntax:

// Normal function with positional arguments
pub fn positional(a: i32, b: i32) -> i32;

// Function with named arguments
pub fn named(pub a: i32, pub b: i32) -> i32;

In the second function, we have explicitly marked the arguments as public.

Why pub?

  • pub is already a keyword.
  • Using it here would coincide with it’s meaning: “making something public / part of the API”.
  • The use of pub would make it very clear that this argument is facing the outside world, making it part of the API and everything that that implies.

API authors would be able to start with positional arguments, like we have now. Until they find they have found a good enough API design. At that point they can just mark all or some arguments as public without any breaking change.

Backwards compatible

This proposal is 100% backwards compatible. There is no change in behavior unless arguments are explicitly marked as public, in which case users will be able to call the function or method with either positional arguments or named arguments.

Future proof

Named arguments are often tightly coupled with default arguments because they complement each other very well. In this RFC I intentionally restrict the scope to named arguments because default arguments often spark lengthy debates over the “good” patterns to use in API design. This means however that we have to make sure in this RFC that we are not closing any doors for the future.

As far as I can tell, this proposal is a relatively isolated syntactic sugar. Default arguments could easily be added in the future:

// Function with default named arguments
pub fn default_arguments(pub a: i32 = 0, pub b: i32 = 2) -> i32;

(Syntax to be determined by a future RFC)

Other clarifications

  • Once you make an argument public, the user can use both forms (positional or named). This allows API authors to “promote” positional arguments to named arguments without any breaking change.
  • A positional argument can not appear after a named argument both in the declaration as well as at the call site.
  • Order of named arguments does not matter, they are resolved by their name and not by their order / position.

Drawbacks

Not sure if any?

Alternatives

Do nothing

Currently you can improve explicitness by wrapping the arguments in structs or use the builder pattern.

struct Window { /* Some fields */ }

struct Position { x: i32, y: i32 }
struct Dimension { width: u32, height: u32 }

impl Window {
   pub fn new(title: &str, pos: Position, dim: Dimension) { /* Some code */ }
}

fn main() {
   Window::new(
       title: "Title",
       Position{ x: 20, y: 50 },
       Dimension{ width: 500, height: 250 }
   );
}

In this example, it works, and some could argue it is even the better choice. But what about

free_fall(z0: 100.0, v0: 0.0, g: 9.81);

There is nothing to group. You could make a FreeFallArgs struct but this would require more boilerplate for a more verbose syntax.

Struct sugar

Add syntactic sugar to make structs more ergonomic to use in functions. For example:

struct FooParams { a: u32, b: u32, c: u32 }

foo({a: 1, b: 2, c: 3});

The braces could even be dropped, it would be less obvious that it is a struct though.

Grouping arguments into structs where it makes sense and judicious use of the builder pattern are good design patterns. But I don’t think they are the most ergonomic solution in all situations.

Named-only arguments

Arguments that can only be called with their named form and not by their position, could prove useful to enforce better APIs. For example, API authors could require booleans to be named-only arguments. This would force the user to be explicit at the call-site and makes bad unmaintainable code just a little bit harder to write.

The downside is that you loose the ability to “promote” positional arguments to named arguments without braking change.

See PEP-3102 for more background.

Parameters trait

Add a special trait, for example Params: Default and have syntactic sugar for invoking functions that have their last argument implementing Params.

Also add syntactic sugar for automatically defining an anonymous params struct.

For example:

#[derive(Params, Default)]
struct ExplicitParams {
    herp: u8,
    derp: i8,
}

// Use this form when you have many parameters
fn explicit(durr: &str, named: ExplicitParams) { ... }

// Use this from when you only have few parameters
// The params struct is automatically defined for you which takes away a lot of the verbosity
fn implicit(foo: &str = "xy", bar = 1u8) { ... }

fn main() {
    explicit("unnamed", derp = 2);
    // desugared: explicit("unnamed", ExplicitParams { derp: 2, ..Default::default() })

    implicit(bar = 2);
    // desugared: implicit(AnonymousStruct { foo: "xy", bar: 2 })
}

This would also take care of default arguments at the same time!

See: https://github.com/rust-lang/rfcs/issues/323#issuecomment-165184703 for the original proposal.

Unresolved questions

No unresolved questions currently.


The discussion has been very long, here is a summary of the most important points raised.


Raising the bar for introducing new syntax
#2

Speaking of default and keyword arguments, how do they interact? More precisely, can keyword arguments be used as positional arguments? If they can?

Assuming you have following function:

 pub fn default_keyword(pub a: i32 = 0, 
                        pub b: i32 = 0, 
                        pub c: i32 = 0, 
                        pub d: i32 = 0)

What is value of parameters:

 default_keyword(0, c:2)
 default_keyword(b:0, c:0, 3)

#3

At the moment this proposal is only focusing on keyword arguments. So you could not just omit arguments if this RFC gets implemented. What you can do however is change the argument order when using the keyword form.

Of course, default arguments can be added later with their own RFC. And as you can see, there are more design trade-offs and unresolved questions that have to be considered for default arguments. That’s why I decided to tackle them separately.

Yes! Keyword arguments would be an additional way to call a function. This allows API authors to “promote” positional arguments to keyword arguments without any breaking change. I guess I should mention that more clearly in the RFC :slight_smile:


#4

Hm, what about mixing positional and named parameters? That is allowed, right? But it might need more specification E.g. default_keyword(b: 0, c:0, 0,0) would be forbidden, right? But default_keyword(0, b: 0, c:0, 0) .


#5

The easiest and most intuitive way is to disallow positional arguments after keyword arguments.

fn foo(pub a: u32, pub b: u32, pub c: u32) {}

// Positional arguments
foo(0, 0, 0)

// Keyword arguments
foo(a: 0, b: 0, c: 0)

// Mixed
foo(0, 0, c: 0)    // Allowed
foo(0, b: 0, c: 0) // Allowed
foo(0, c: 0, b: 0) // Allowed
foo(a: 0, 0, 0)    // Disallowed

Of course I’m open to alternative behaviors if they are not too confusing.


#6
f(x: y);

Is that a function call with a named argument x with value y, or a function call with a positional argument that’s coercing x to type y?


#7

I think this RFC should mention Pythonish keyword-only arguments (PEP) in alternatives or future compatibility sections.


#8

Obviously, we should follow the precedent that name: value is valid within curly braces, and allow calling functions using them:

f{x: y};

:wink:


#9

Please call this “named arguments” since keywords are already a different thing.


#10

In case anyone actually takes you seriously, that would be a breaking change given that the following is valid:

struct foo { x: i32 }
fn foo(x: i32) {}

fn main() {
    foo{x: 1};
    foo(1);
}

#11

I hope not!


#12

Named argument x with value y. I took the same tokens as for struct initialization, but obviously this can be changed to foo(x=y), foo(x=>y) or anything else that would make sense.

Would there be any advantages to have keyword only arguments in Rust?[quote=“jethrogb, post:9, topic:3831, full:true”] Please call this “named arguments” since keywords are already a different thing. [/quote]

Sure, but for my education, what is the difference?


#13

Could you give an example of that would create that kind of coercion? I can’t seem to recall ever seeing an example of such coercion.


#14

Keywords are words that have special significance in the language, such as fn, struct, self, etc.


#15

#16

I think yes. Named only args can help to enforce API consistency. For example, it can be useful to make a bool argument named only.

And I think the original PEP rationale about mixing varargs and optional args also applies:

// I want to call `foo` with or without `opt`. 
// It is possible if `opt` is named only
fn foo(opt: String, args...: String)

To be clear, I don’t say that we do or do not need this feature in Rust, I just think it’s worth mentioning in the RFC :slight_smile:


#17

@apasel422 Thanks for the link. I didn’t really get why @DanielKeep was thinking about type coercion, now I understand. But it would be a little silly to put type ascriptions in function calls considering they are already explicitly typed, no?

That is indeed a compelling argument, I will add it as alternative. The downside is that “promoting” a positional argument to a named argument would then be a breaking change. I’m not sure which quality is better to have.

Does that preclude that they can have alternate meanings, especially when combined with other words? Python seems to call them keyword arguments.


#18

@apasel422: Is type ascription behind feature gate or is it on stable?


#19

It’s currently gated.


#20

Doesn’t have struct initialization the same problem? If yes, it should be handled consistently.