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

Wishlist: functions with keyword args, optional args, and/or variable-arity argument (varargs) lists #323

Open
pnkfelix opened this issue Sep 25, 2014 · 259 comments

Comments

@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Sep 25, 2014

A portion of the community (and of the core team) sees one or more of the following features as important for programmer ergonomics:

  • keyword-based parameters (as opposed to position-based parameters),
  • optional parameters (where one provides a default value for them, usually in the parameter list),
  • variable-arity functions (which can be seen as a generalization or variation on optional parameters, depending on how you look at it).

This issue is recording that we want to investigate designs for this, but not immediately. The main backwards compatibility concern is about premature commitment to library API's that would be simplified if one adds one or more of the above features. Nonetheless, We believe we can produce a reasonable 1.0 version of Rust without support for this.

(This issue is also going to collect links to all of the useful RFC PR's and/or Rust issues that contain community discussion of these features.)

@flying-sheep
Copy link

@flying-sheep flying-sheep commented Oct 8, 2014

Nonetheless, We believe we produce a reasonable 1.0 version of Rust without support for this.

depends on how you see it. the API will definitely be well-considered given the constraints of no default/kw arguments.

but once those are added, i’m sure the API will be considered lacking, especially in areas where new defaults render old functions obsolete.

a good example (if the new syntax sugar wouldn’t exist) would be ImmutableSlice:

fn slice(&self, start: uint, end: uint) -> &'a [T];
fn slice_from(&self, start: uint) -> &'a [T];
fn slice_to(&self, end: uint) -> &'a [T];

slice_from and slice_to will be immediately obsolete once you can just leave out start or end from slice. i bet there are hundreds of examples more.

@pnkfelix
Copy link
Member Author

@pnkfelix pnkfelix commented Oct 9, 2014

@flying-sheep so then you deprecate such methods, just like today, no?

@reem
Copy link

@reem reem commented Oct 9, 2014

@pnkfelix I think his argument is that we don't want to be stuck with a ton of deprecated cruft in the stdlib, but I'm still personally not too sympathetic to the need to have default arguments before 1.0.

@flying-sheep
Copy link

@flying-sheep flying-sheep commented Oct 9, 2014

yeah, that’s my argument. either cruft, or lengthy deprecation formalities and rust 2.0 a year after 1.0 (semver requires a major version bump for breaking changes)

@Valloric
Copy link

@Valloric Valloric commented Oct 10, 2014

While I'd prefer to have optional/keyword args before 1.0, I believe the problem with deprecated functions crufting up the API can be substantially lessened by removing such functions from the generated API docs. This is how the Qt project (and D AFAIK) handles API deprecation; the deprecated stuff continues working but developers writing new code don't see it.

Of course, the generated docs should have a setting/link/button to show the deprecated API items but it should be off by default.

I think this is also a good idea in general; just a couple of days ago I accidentally used a deprecated function because it seemed like a good pick and I didn't notice the stability color.

@sfackler
Copy link
Member

@sfackler sfackler commented Oct 10, 2014

Rustdoc's handling of deprecated items definitely needs some improvement - see rust-lang/rust#15468 for some discussion.

@aturon aturon mentioned this issue Feb 3, 2015
@aturon
Copy link
Member

@aturon aturon commented Feb 3, 2015

See the "Struct sugar" RFC for another take.

@pnkfelix pnkfelix changed the title Wishlist: functions with keyword args, optional args, and/or variable-arity argument lists Wishlist: functions with keyword args, optional args, and/or variable-arity argument (varargs) lists Mar 11, 2015
@gsingh93
Copy link

@gsingh93 gsingh93 commented May 18, 2015

I'd like to see some of these RFCs revived in the near future, if someone has time to do so.

@aldanor
Copy link

@aldanor aldanor commented May 23, 2015

Agreed, there's a whole bunch of different keyword arguments proposals floating around and there's been a few discussions which seemed to die off a few months ago... would love to hear the current standpoint on this.

@e-oz
Copy link

@e-oz e-oz commented Dec 4, 2015

Ok, 1.0 released, even more, can we please discuss it again? especially default arguments.

@steveklabnik
Copy link
Member

@steveklabnik steveklabnik commented Dec 4, 2015

This issue is open, it's free to discuss.

@pnkfelix
Copy link
Member Author

@pnkfelix pnkfelix commented Dec 4, 2015

This issue is open, it's free to discuss.

(though its possible an https://internals.rust-lang.org post might be a better UI for undirected discussion ... we didn't have the discuss forums when we set up these postponed issues...)

@yberreby
Copy link

@yberreby yberreby commented Dec 10, 2015

I'd love to see keyword arguments. I opened a thread on /r/rust with some comments about them before finding this issue. I guess /r/rust is an appropriate place for "undirected discussion" too?

@ticki
Copy link
Contributor

@ticki ticki commented Dec 10, 2015

In any case, this should be done in such a manner that it does not cause very inconsistent libraries, perhaps by letting the named parameters be optional? For example the names could be given by the argument name. Such that, the function:

fn func(a: u8, b: u8) -> u8;

can be called both with and without named parameters, for example:

func(a: 2, b: 3)

or something along this, while still being able to do:

func(2, 3)

@ticki
Copy link
Contributor

@ticki ticki commented Dec 10, 2015

Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing.

@golddranks
Copy link

@golddranks golddranks commented Dec 10, 2015

I think it's because supposedly people are thinking about some kind of heterogenous variadicity (like the case of println, which is currently done with macros), and that isn't possible with arrays.

@ticki
Copy link
Contributor

@ticki ticki commented Dec 10, 2015

I see, but that's why we got macros. If you want heterogenous variadicity, you gotta go with macros, after all the Rust macro system is very powerful.

@yberreby
Copy link

@yberreby yberreby commented Dec 10, 2015

I agree, macros are appropriate for this.

@oblitum
Copy link

@oblitum oblitum commented Jan 23, 2020

In my opinion, in a real ML language with currying, named parameters improves composability, since you can more easily curry a function without being constrained to positional parameters. See OCaml/Reason for example.

@oblitum
Copy link

@oblitum oblitum commented Jan 23, 2020

I'm not really against such structural records proposal, except for the call-site syntax which isn't great. Not sure what's better for Rust in the end, and was just asking on a generic sense why it would not be great for composition, maybe Rust can have stumbling blocks for the concept.

@oblitum
Copy link

@oblitum oblitum commented Jan 23, 2020

fn foo(hof: impl Fn(u8)) { // <-- how do you call the HoF with keyword args?
   hof();
}

For generic coding, my first thought is simply that a function would match such traits based on types, not names/labels. You can view foo as the label of the function as well, and parameters can have labels, but would not matter when matching a function type, just like the function name doesn't. Warning that I didn't thought about it thoroughly.

// Ostensibly you could do the following, but that adds more special cases:

fn foo(hof: impl Fn(pub name: u8)) {
   hof(name: 42);
}

Indeed, but that's in your setting where labels make part of signature, which is not compatible with previous scenery, where I think the Fn signature can even freely have different labels, but it would still match any function based on the types, which even makes for a feature where a library author can put parameter labels in traits and make use of them, and a non-labeled function can still match, so you would be late-naming parameters, which is an interesting concept. I always viewed named parameters as a killer feature for end user, not type authoring, so I don't have much background on the idea of incorporating labels to type signature (your view) and using it for traits and composition at that level. I think all the discussion revolves more around end user code having to call functions than library author having to compose types.

@JesterOrNot
Copy link

@JesterOrNot JesterOrNot commented Feb 14, 2020

I would love this as someone who likes to write libraries something like

fn color_print(text: &str, bold: bool=false, underline: bool = false) {
    // ASCII STUFF
}

Could be called as

color_print("Hello", underline=true);

instead of having to define bold as well
Is there a time table on this?

@oblitum
Copy link

@oblitum oblitum commented Feb 14, 2020

Is there a time table on this?

This issue is all about discussing this topic, and there's no timeline to it ever becoming supported syntax at the moment.

@GoldsteinE
Copy link

@GoldsteinE GoldsteinE commented Mar 23, 2020

I would love this as someone who likes to write libraries something like

fn color_print(text: &str, bold: bool=false, underline: bool = false) {
    // ASCII STUFF
}

Could be called as

color_print("Hello", underline=true);

instead of having to define bold as well
Is there a time table on this?

It can be done like this in current Rust, and boilerplate impl can be generated with macro.

@robinmoussu
Copy link

@robinmoussu robinmoussu commented Mar 23, 2020

Fluent interfaces are nice, but:

  • it adds a lot of boilerplate
  • if the bolerplate is generated by a macro, and if fluent interface are used a lot, the compile time increase significantly
  • it makes api discoverability harder
  • if you want to have named argument that implement a trait (like a closure), fluent API are much harder to write as soon as you have more than one argument, and increase a lot of the code that need to be generated (each combinaison of trait is going to be monomorphased).

@mattiasdrp
Copy link

@mattiasdrp mattiasdrp commented Mar 27, 2020

Well, it's been a long trip reading all the comments for this issue from the beginning. I'd just like to add my 2 cents here. I'll obviously talk a bit about OCaml but I've been interested more and more in Rust and I really love this language even though I think not having labeled arguments and optional arguments is a bit awkward.

Labeled arguments are described like this in the OCaml manual

Labeled arguments are a convenient extension to the core language that allow to consistently label arguments in the declaration of functions and in their application. Labeled arguments increase safety, since argument labels are checked against their definitions. Moreover, labeled arguments also increase flexibility since they can be passed in a different order than the one of their definition. Finally, labeled arguments can be used solely for documentation purposes.

For instance, the erroneous exchange of two arguments of the same type —an error the typechecker would not catch— can be avoided by labeling the arguments with distinct labels.

First, I don't think labeled arguments (or, as you call them, keyword arguments) should be enabled by default, I think it needs to be opted-in. In OCaml the syntax for labeled argument is

let f ~x ~y = x + y
let _ = f ~x:2 ~y:3
let x = 2 and y = 3 in f ~x ~y

As in records, if a variable has exactly the same name as a labeled argument, you don't have to write ~x:x and can simply write ~x

Which would give, in Rust

fn f(~x: i32, ~y:i32) -> i32 { x + y }
let _ = f(~x:2, ~y:3)
let x = 2;
let y = 3;
let _ = f(~x, ~y)

(Notice the body of the function not using the ~. This should only be used when declaring and using the function)

This syntax allows using labeled and anonymous arguments in the same function

let f x ~y = x + y
fn f(x: i32, ~y:i32) -> i32 { x + y }

In the propositions I saw, I liked the fn(pub x:i32) but I don't really like the fact that we're using the pub keyword for another meaning.

As for optional arguments, in OCaml only labeled arguments can be optional and you can either give them a default value or not (in the latter case they will be interpreted as an option)

The syntax replaces the ~ by a ?

 let f ?(x = 2) y = x + y
(* val f : ?x:int -> int -> int = <fun>*)

Which, in Rust, could also be implemented:

fn f(?(x = 2): i32, y:i32) -> i32 { x + y }

When no default value is given in the function declaration, the core of the function takes an option containing Some(value) or None which may be confusing since the type of the argument (i32) is not the type in the function's core (Option<i32>)

My opinion on this after using them for many years is that they prevent some errors (try making a game with x and y positions everywhere with the ones who want x, x', y, y' to move the sprites and the others who want x, y, x', y' and some want x, dx, y, dy). They give more readability to your program (not the function definition but mostly the function calls). Optional arguments allow to have simple declarations (when I move up do I want a move_up function or just a move function which defaults to dx = 0 or x' = x ? I could give 0 as an argument to my move call but it adds noise for nothing. There are a lot of other examples like this)

I'd really like these two features to be implemented in a near future (and I really don't think that builder pattern comes anywhere close to the practicality of labeled and optional arguments in terms of readability, usability etc.)

Rust having been compiled initially in OCaml, I'm sure the original devs used labeled arguments and know how practical they are in some cases and yes, I read all the arguments written here but we've had them for years now in OCaml and the programs are still well written by good programmers.

Please, consider it in the nearest possible future :-)

@mattiasdrp
Copy link

@mattiasdrp mattiasdrp commented Mar 27, 2020

I'd like to add one thing that came to my mind later. Optional arguments already exist in Rust when you use range.
[i..j] is interpreted as "from i to j"
[..j] is interpreted as "from start to j"
[i..] is interpreted as "from i to end"
[..] is interpreted as "from start to end"

So we have 2 optional values that default to start and end and they are automatically filled when they are not provided. (and, yes, optional parameters should be named for safety reasons)

And for those who keep defending the builder pattern, you do realise that they only work when working with records which is not the sole purpose of optional and labeled arguments, I hope.

Labeled and optional arguments are zero-cost at runtime since they are just syntactic sugar, I don't see any valid reason to not implement them.

@jstasiak
Copy link

@jstasiak jstasiak commented Mar 27, 2020

Relatively long time observer of this thread here. @OCamlPro-mattiasdrp: your posts are a great summary of the relevant parts of the subject to me and I almost fully agree with them. I have one comment though (otherwise I'd not be writing this at all), it concerns this part:

As in records, if a variable has exactly the same name as a labeled argument, you don't have to write ~x:x and can simply write ~x

Which would give, in Rust

fn f(~x: i32, ~y:i32) -> i32 { x + y }
let _ = f(~x:2, ~y:3)
let x = 2;
let y = 3;
let _ = f(~x, ~y)

I believe to really match what happens in structs, the last statement would read

> let _ = f(x, y)

just like there's no extra syntax when using struct field initialization shorthand. But it's just a detail. As a person coming from Python I really miss named and optional arguments, the syntax is not that important to me.

@mattiasdrp
Copy link

@mattiasdrp mattiasdrp commented Mar 27, 2020

Oh well, I actually don't have a preference over the syntax used to call labeled arguments but the thing is, if you want to mix them with positional ones I think you have to specify if an argument in a function call is attached to a labeled one or a positional one.

Let me explain.

I actually see labeled argument as records. The reason why modern languages don't use only tuples is because you can get lost using them so instead of having an (i32, i32, i32) record I would have a {r: i32, g: i32, b: i32} one which tells me immediately what I'm doing with it. The thing is, structs don't have positional arguments and tuples don't have labeled arguments whereas functions could.

Let's say I have

fn f (x: i32, ~y:i32, ~z:i32) { x - y - z }
let y = 2;
let _ = f(y, y, ~z:y)

It can be confusing knowing which one is attached to a labeled argument and which one isn't (and it becomes really hard when you have optional arguments).
So my personal preference goes toward calling them the same way they're declared:

let _ = f(y, ~y, ~z:y)

But that's just preference and I'll actually be happy if they just finally implement them in the standard library since there are a lot of clues of them being implementable with records, the .. syntax to omit some parts when defining a new record with an old one and the optional bounds in ranges.

@jstasiak
Copy link

@jstasiak jstasiak commented Mar 27, 2020

Fair enough, I'm convinced now this (what you said) is better.

@robinmoussu
Copy link

@robinmoussu robinmoussu commented Mar 27, 2020

I've been following this thread since quite some time but don't see any real progress. What can be done in order to finally make things move. I fear that I don't have the competence and the time to modify rustc and do a PR but I would like to see things going forward.

@cindRoberta
Copy link

@cindRoberta cindRoberta commented Apr 27, 2020

default parameter:

fn foo(a: i8, b: f32 = 5.6, c: Option<bool> = None) {}

foo(7);
foo(7, 6.9);
foo(7, 6.9, Some(true));

@yulincoder
Copy link

@yulincoder yulincoder commented Jun 14, 2020

I can't really do much, but I think this feature is necessary.

@joemooney
Copy link

@joemooney joemooney commented Jun 22, 2020

Rust noob here...what about requesting very limited initial support?

fn foo(a: i8, b: f32, c: Option<bool>) {}

foo(7, 6.9);  // Trailing Option<T> args are replaced with None
foo(7, 6.9, Some(true));

Maybe to help readability, named parameters that are either just documentation that is discarded by the compiler or checked that they are named and ordered correctly:

fn foo(a: i8, b: f32, c: Option<bool>) {}

foo(a:7, b:6.9);  // Trailing Option<T> args are replaced with None by compiler
foo(7, 6.9, c:Some(true));  // would prefer f(7, 6.8, c:true)
foo(b:7, a:6.9);  // compiler error

@Ixrec
Copy link
Contributor

@Ixrec Ixrec commented Jun 22, 2020

It's probably worth mentioning that the most recent constructive discussion of named arguments in general that I know of was at https://internals.rust-lang.org/t/named-arguments-increase-readability-a-lot/12467.

AFAIK there simply is no minimal subset of this which avoids dealing with any of the really big, awkward questions that any named arguments proposal has to grapple with, which I tried to summarize early in that thread, and much of the rest of the thread discusses in more detail. What you just described is definitely among the pile of existing suggestions and hits a lot of those known issues.

@joemooney
Copy link

@joemooney joemooney commented Jun 23, 2020

Thanks!

@ddouglas87
Copy link

@ddouglas87 ddouglas87 commented Jul 26, 2020

If someone compiled the list of proposed solutions and then we all had a vote on it, would that help move this issue forward?

@Keats
Copy link

@Keats Keats commented Jul 27, 2020

There is a RFC for named arguments: #2964

@carnoxen
Copy link

@carnoxen carnoxen commented Aug 3, 2020

+1 for default parameter. It is not recent technics, which c++, js already have.

fn factorial(n: i32, acc = 1) -> i32 {
    if n <= 1 {
        acc
    }
    factorial(n - 1, acc * n)
}

@Spleeding1
Copy link

@Spleeding1 Spleeding1 commented Oct 31, 2020

First let me say that I am surprised after 6 years this issue is still unresolved.

Second, there is already a process for Optional parameters and default parameters, the downside being that you always have to add Some() or None to the function call. I think there just has to be a new way of using the existing Options:

// Current Option<T>
fn feelings(happy: Option<bool>) {
    let happy = happy.unwrap_or(true);
    
    if happy {
        println!("I am happy!");
    } else {
        println!("I am not happy!")
    }
}

feelings(Some(true));  // ok
feelings(Some(false)); // ok
feelings(None);        // ok
feelings(true);        // not ok
feelings(false);       // not ok
feelings();            // not ok

// Recommended *Option<T> - defaults to None
fn feelings(happy: *Option<bool>.unwrap_or(true)) {
    if happy {
        println!("I am happy!");
    } else {
        println!("I am not happy!")
    }
}

feelings(Some(true));  // ok
feelings(Some(false)); // ok
feelings(true);        // ok
feelings(false);       // ok
feelings(None);        // ok
feelings();            // ok

So if using *Option<T> in a function or method for a parameter would .unwrap() it before running the function, and if no value was given would default to None. Then you can use unwrap_or(my_value) to implement a default value for that parameter.

I think a little example can clearly explain the pros of named parameters. I went with the . for kwarg identifier because of this RFC seeming to have popularity. I also know this probably isn't the greatest example, but I think it makes the point. I got the idea from another thread which I don't remember where it was.

// fn with named optional params lines of code = 21

fn print_the_word(
    .word: &str,
    .bold: *Option<bool>.unwrap_or(false),
    .italic: *Option<bool>.unwrap_or(false),
    .underline: *Option<bool.unwrap_or(false)
) {
    let mut final_word = &word;

    if bold {
        // Make final_word bold
    }
    if italic {
        // Make final_word italic
    }
    if underline {
        // Make final_word underlined
    }
    println!("{}", &final_word);
}

print_the_word(.word: "Different!");

// Struct lines of code = 35
// Always have to use ..Struct.default()

struct Word {
    word: &str,
    bold: bool,
    italic: bool,
    underline: bool
}

impl Default for Word {
    fn default() -> Self {
        Word {
            word: "Word!",
            bold: false,
            italic: false,
            underline: false
        }
    }
}

impl Word {
    fn print_the_word(&self) {
        let mut final_word = &self.word;
        if self.bold {
            // Make final_word bold
        }
        if self.italic {
            // Make final_word italic
        }
        if self.underline {
            // Make final_word underlined
        }
        println!("{}", &final_word);
    }
}

Word { word: "Different!", ..Word.default() }.print_the_word();

I don't think this has to break backwards compatibility. A function definition has to use either all args or all kwargs, not a mix. Same for calling the function. If using the args, they have to be in the correct order, kwargs can be out of order.

// straight args
fn arg_fn(one: &str, two: bool, three: u8){
    // function body
}

fn kwarg_fn(.one: &str, .two: bool, .three: u8){
    // function body
}

arg_fn("one", false, 10);

// kwarg without names
kwarg_fn("one", false, 10); // ok
kwarg_fn(.one: "one", .two: false, .three: 10); // ok
kwarg_fn(.one: "one", .three: 10, .two: false); // ok
kwarg_fn("one", false, .three: 10) // not ok

So updating of crates to kwargs can be done without breaking, as long as the order of the parameters doesn't change.

@RustyYato
Copy link

@RustyYato RustyYato commented Dec 24, 2020

You can use impl Into<Option<T>> on stable and then your able todo

fn foo(x: impl Into<Option<bool>>) { let x = x.into(); }
foo(true);
foo(Some(true));
foo(None);

Not that I recommend actually doing this.

@dbsxdbsx
Copy link

@dbsxdbsx dbsxdbsx commented May 13, 2021

@Spleeding1 @pnkfelix , I hope this feature could be added as a feature in the coming version RUST 2021 just like the feature fstring:implicit format arguments. Hopefully, this could make it more feasible to transfer python code to rust.

@tanriol
Copy link

@tanriol tanriol commented May 13, 2021

@dbsxdbsx I don't think f-strings are coming soon, only implicit format arguments are. As for default/optional/keyword arguments, that's really unlikely - Rust 2021 is too soon and there's no consensus yet on a final design for this (and even no consensus what the requirements actually are, I think).

@lebensterben
Copy link

@lebensterben lebensterben commented May 13, 2021

@tanriol
There's no consensus whether to include kwargs or optional args in the first place.

@dbsxdbsx
Copy link

@dbsxdbsx dbsxdbsx commented May 13, 2021

@tanriol, maybe I missed the point? I just saw the f-string like feature in new plan for Rust 2021.

@tanriol
Copy link

@tanriol tanriol commented May 13, 2021

@dbsxdbsx The point of the 2021 edition is not to include every feature, but to make minor breaking changes that some features may need. The edition will remove some legacy panic! behavior to enable implicit format arguments, but the feature itself can come in later Rust versions.
Also note that the current implementation will not provide "real" f-strings like f"{name}", but only special support for format!-style macros.

@lebensterben Sure, that was implied in the "what the requirements are" section.

@zoombinis
Copy link

@zoombinis zoombinis commented Aug 21, 2021

(one of) the biggest use cases for default arguments: stubbing

Whenever I need to unit test a function that relies on a difficult-to-mock procedure (such as data access, etc.), while I could try to mock it (certainly more code), by instead stubbing the procedure as a default to a function argument, none of the calling code upstream is impacted (simplest/lowest risk change) and this also enables my unit test to then override the stub with a dirt-simple function, returning whatever dummy data is useful for testing purposes.

As it typically goes with functional programming, it also comes with a secondary benefit that my production code is optionally more modular, again with no upfront refactor cost to any upstream callers of this function.

So in a perfect world, I agree you can live without default args (and as proven by Rust to-date). But they ease refactoring for testing in imperfect environments and schedules that cannot afford complex mocking or changes to a calling signature.

By the way, I couldn't be happier with Rust. Thank you for what is already an incredible language. I do hope this RFC makes it through the gates - it is the most popular if you go by "thumbs up" count.

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

Successfully merging a pull request may close this issue.

None yet