A portion of the community (and of the core team) sees one or more of the following features as important for programmer ergonomics:
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.)
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.
@flying-sheep so then you deprecate such methods, just like today, no?
@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.
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)
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.
Rustdoc's handling of deprecated items definitely needs some improvement - see rust-lang/rust#15468 for some discussion.
See the "Struct sugar" RFC for another take.
I'd like to see some of these RFCs revived in the near future, if someone has time to do so.
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.
Ok, 1.0 released, even more, can we please discuss it again? especially default arguments.
This issue is open, it's free to discuss.
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...)
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?
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)
Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing.
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.
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.
I agree, macros are appropriate for this.
When I was first learning Rust pre-1.0, hearing that keyword arguments were not going to be implemented for 1.0 seemed like a terrible choice. From a background in Ruby/Python/JavaScript, keyword arguments are a very common and natural way to create nice APIs.
However, after actually using Rust for a while, I've actually grown to like the struct/builder pattern _much_ more than keyword arguments. The problem with making keyword arguments easy is that it encourages functions that do too much by taking a bunch of different options.
If, instead, you are restricted to positional arguments, then you're more inclined to keep functions small, with a smaller surface area for their signatures. If there a small number of variations on the behavior of a function, these can exist as different named functions (e.g. alternate constructors.) If there really are a large number of variables needed for a function, pulling those variables out into a dedicated struct provides basically all the benefits of keyword arguments, plus the possibility of reuse.
I think if keyword arguments are added, they should just be syntactic sugar for functions that take a struct argument.
If you want heterogenous variadicity, you gotta go with macros, after all the Rust macro system is very powerful.
And then you have to limit the length of your heterogeneous lists to ~10 to avoid horrible metadata bloat.
Macros help, but it's still a half-measure.
@jimmycuadra I agree. Functions should be small and compact, however sometimes you want to be able to know the argument order without looking up the signature (which keyword arguments solve), even though structs and other type constructions should always be prefered over named parameters, when dealing with larger amount of inputs.
One thing that's important to me is consistency with the current call syntax (such that it's up to the caller whether to use named arguments or not). This can be achieved in the way I described in my previous comment.
@petrochenkov The problem you outline is a problem with macros, not a need for method variadicity.
Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing.
No, using structs instead of named arguments is a bad thing. And using array instead of arguments is a horrible thing.
I agree, macros are appropriate for this.
For named arguments? Or for optional arguments? Writing macros for EACH function, export/import it - it's very over-verbose.
I think if keyword arguments are added, they should just be syntactic sugar for functions that take a struct argument.
No way. Better not implement it at all than this.
Functions should be small and compact
Let each user decide how their functions should look. Don't roll into Go-ideology "we don't have it because you don't need it and we know better".
Really frustrated to see such comments. All modern languages except Go have it, but Rust "don't need it, because macros". What a shame.
No, using structs instead of named arguments is a bad thing.
Not true. Structs are strongly typed (hence less prone to errors) and makes it possible to efficiently reuse codes. Your functions shouldn't take a large number of arguments anyway.
And using array instead of arguments is a horrible thing.
This is not true. Using arguments instead of arrays is a horrible thing.
For named arguments? Or for optional arguments? Writing macros for EACH function, export/import it - it's very over-verbose.
It not very often you run into lack of variadic functions, and when you do, you can write a macro, and when generic integers lands, you can just use arrays. Often _functions_ takes a fixed number of arguments anyways.
Also, note that the macro syntax will soon be revised.
Let each user decide how their functions should look. Don't roll into Go-ideology "we don't have it because you don't need it and we know better".
One of Rust's design philosophies is that Rust should not lead the user into bad code, bugs, and logic errors (this is the reason why we don't have increments, for example). We're all lazy after all, but there is no reason to encourage being lazy.
Your functions shouldn't take a large number of arguments anyway.
I will decide it, not you.
Structs are strongly typed
function arguments too.
Often functions takes a fixed number of arguments anyways.
If _your_ functions often take fixed number of arguments, all other users should obey?
One of Rust's design philosophies is that Rust should not lead the user into bad code
And again - what is bad code will decide user, not some very arrogant community members.
If your functions often take fixed number of arguments, all other users should obey?
I'm just saying that introducing syntactic sugar for such a small special case is not worth it. This job is better suited for macros.
And again - what is bad code will decide user, not some very arrogant community members.
Sure, user should not be forced to not writing bad code, but neither should they be encouraged to.
Just a friendly reminder everyone: let's try to keep conversation civil and constructive! Thanks.
Are keyword-based arguments must be mandatory for all or it is up to me?
@KalitaAlexey I'm not exactly sure what you're asking, but I supose you mean "Is it mandatory to provide the name of the parameters to the function, if we get keyword arguments". And the answer is no, because that would break backwards compatibility.
So. With keyword arguments there is should be possibility to write
fn addObject(object: Object, flag: bool);
fn addObject(object: Object, size: usize);
Is it?
Sure, user should not be forced to not writing bad code, but neither should they be encouraged to.
And there is no =
sign between "optional arguments", "named arguments" and "bad code". Maybe some personal opinions only. For somebody 3 arguments it's too much, for somebody it's ok. Nobody should be forced to ask "can I use 4 arguments in this function please?".
I guess this issue is about how to add these features properly or conclude that there is no proper solution, rather than debate if we support them or not.
After using OCaml for a few months, I'm a huge fan of keyword arguments. I have a lint for when keyword arguments are used positionally, and I'd be a fan of allowing something similar in Rust.
Edit: to clarify, this is a +1 for syntactically allowing keyword arguments to be used positionally, if we can also get an optional lint/warning of some kind for when this happens.
Just a friendly reminder everyone: let's try to keep conversation civil and constructive! Thanks.
+1. It's incredibly easy to start discrediting ideas when they come from a rude source.
If you want heterogenous variadicity, you gotta go with macros, after all the Rust macro system is very powerful.
@Ticki This is definitely how it is now, but I haven't seen an inherent argument for keeping it this way. That said, in my experience, variadic functions are few and far between, so I don't necessarily feel that the relatively incremental gain of variadic functions is worth the extra language and compiler complexity. The only difference to the user is an extra !
character. It is a _little_ heavier on the implementation side, but not by a huge amount, and I've heard whispers of a macro system overhaul that might even make this nicer?
Let each user decide how their functions should look. Don't roll into Go-ideology "we don't have it because you don't need it and we know better".
@e-oz I agree with this. I feel it both condescending and incredibly frustrating when a language or library does not support something because in the past, the authors decided what a user should and shouldn't do. Obviously Rust enforces lots of things that users shouldn't do, but it's pretty good about giving escape hatches to users who say "no, I _really_ do want to do this".
Functions should be small and compact, however sometimes you want to be able to know the argument order without looking up the signature (which keyword arguments solve)
@Ticki This is to me one of the huge benefits of keyword arguments. It's easier for not only the writer, but the reader of code later! It also makes it harder to accidentally pass same-typed arguments in the wrong order.
...even though structs and other type constructions should always be prefered over named parameters, when dealing with larger amount of inputs.
@Ticki I don't see why this is true? Structs and builders are a lot more boilerplate for the same functionality as keyword + optional arguments.
Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing.
@Ticki One lovely place to use keyword arguments is in constructors. It means the struct internals can remain private, while having all the nicety of position-independent labeled struct creation.
The problem with making keyword arguments easy is that it encourages functions that do too much by taking a bunch of different options.
@jimmycuadra This is people using a useful tool for poor reasons, and I don't see it as a reason to keep the tool away from everybody else.
@bfops Great comment.
@Ticki I don't see why this is true? Structs and builders are a lot more boilerplate for the same functionality as keyword + optional arguments.
The reason I think struct should be prefered over named arguments is that:
You're able to define methods on struct. You cannot define methods on named parameters (as a collection), due to their "untyped" nature. One thing you might do is to begin to pass what should be a struct as parameters (since this is easy and ergonomical with named parameters).
That said, I'm not against nor supporting it, I'm just providing cases for/against the proposed features.
You're able to define methods on struct. You cannot define methods on named parameters (as a collection), due to their "untyped" nature. One thing you might do is to begin to pass what should be a struct as parameters (since this is easy and ergonomical with named parameters).
@Ticki You can define closures locally, which is essentially equivalent, no? And it only borrows the arguments it actually refers to - with a struct, you can't move/borrow individual members without moving/borrowing all.. I think?
You can define closures locally, which is essentially equivalent, no?
I'm not sure what you're trying to say here. Closures are just anonymous implementing a particular traits.
And it only borrows the arguments it actually refers to - with a struct, you can't move/borrow individual members without moving/borrowing all.. I think?
True.
I think you misunderstood my comment (and it's my fault, I'm horrible at wording my thoughts).
The individual parameters are obviously still typed, but the collection of arguments is not. This is not a problem when there's only 4 arguments. But it is when there are more, which is often the case when you used named parameters.
The nature of the parameters are the same in both cases (named or tuple parameters), but it gets problematic when there are many arguments.
Too many arguments is often a sign of using too few data structures, which, in worst case, can lead to spaghetti code (because of rewriting chunks of code which could just be defined as methods on your struct).
Not sure if any of that made sense?
Ah, so it sounds like what you're saying is not that when a function has many parameters it's necessarily better to group them all as a struct, but rather that it's likely that some of those arguments probably make sense to group into structs? I assumed you were talking about the builder pattern, where the parameters for a given function are put all into a single datatype, meant to be used with that one function.
I'm not sure what you're trying to say here. Closures are just traits.
By this I meant
struct FooParams {
x: int,
y: &'static str,
z: char,
}
impl FooParams {
fn bar() -> float { ... }
}
fn foo(params: FooParams) {
...
}
is roughly equivalent to
fn foo(x: int, y: &'static str, z: char) {
let bar = || {
...
};
...
}
The "methods" on the struct-of-parameters become closures over the relevant parameters.
I ask again. Currently there is no ability to declare two functions with same name. I cannot do
fn add(&mut self, object: Object);
fn add(&mut self, boxed_object: Box<Object>);
If we will support keyword-based arguments then it will be allowed?
@KalitaAlexey No, that's out of the scope of this issue.
@ticki So why then have keyword-based arguments?
@KalitaAlexey because that’s the most natural way to call a function with multiple optional arguments which can be confused in order. e.g. OpenOptions
:
instead of doing:
let f = OpenOptions::new()
// .read(true)
// .write(true)
// .append(true)
// .truncate(true)
// .create(true)
.open("path/to/file");
//or the shorthands:
let f2 = File::open("path/to/file2");
let f3 = File::create("path/to/file3");
the API could have been
let f2 = File::open("path/to/file2", read=true);
let f3 = File::open("path/to/file3", write=true, create=true, truncate=true);
although maybe File::create("path")
as alias for the latter would still have its place…
I'll write up a RFC on default struct fields, which I believe solves both the problem with default parameters and named parameters.
I don't feel that default struct fields address named parameters in satisfying way. Structs add a lot more boilerplate than "first-class" named parameters do. On the other hand, having named + default parameters gives us default struct fields "for free", since Foo::new
can now have named and default parameters.
@bfops I would argue that extending the struct builders for supporting default fields would make structs able to replace named parameters, because you'd then be able to leave fields out giving them a default value.
@Ticki please stop. It's not thread "do we need it or not". If you don't need it - just don't write anything.
i’m sorry but i have to agree with @e-oz. this discussion is misplaced. we’re not talking about structs here.
@Ticki You can already do that in a struct builder, by making the field an option, and initializing it when the builder is "invoked" to create the struct.
My argument is that structs and builders are a lot more boilerplate and heavily disincentivize using named parameters entirely, especially for small functions. As you've said, small functions are much more desirable and common, and making those functions pleasant is a huge priority for me personally.
You can already do that in a struct builder, by making the field an option, and initializing it when the builder is "invoked" to create the struct.
Then you get a performance penality and you'd have to handle the fallback value yourself.
@flying-sheep The issues are orthogonal, and one might be able to replace the other. Sorry for hijacking this thread.
When you say "struct builders", are you talking about the builder pattern, or the struct literal syntax? If you're talking about augmenting the struct literal syntax with default values, then I'd still _definitely_ endorse keyword and default arguments, since this is traditionally the kind of behavior that constructors are meant to handle (and using constructors allows the author to keep struct members private). It sounds like both these options will solve the same set of problems, and keyword and default arguments will lessen the overall amount of boilerplate, since it doesn't force users to always use structs.
Whether it's _good_ to force users to always use structs is a matter of opinion, but this seems to me like arguing against tuples because "users should be using a struct". I would prefer to provide both tools and let users make informed decisions based on their use case.
@bfops What I'm talking about is
struct Foo {
a: u32 = 300, // <- the `300` is the default value of the field a
b: i32, // This field do not have a default value
c: i32, // Neither do this
}
fn my_function() {
let var = Foo {
// Note that I do not define the value of `a` here
b: 42,
c: 999,
... // Fill the rest of the struct with the default values
};
assert_eq!(var.a, 300); // var.a == 300
}
or something like that.
OK, so you’re basically saying that with enough syntactic sugar, structs can become simple enough that they are able to make default/named parameters superfluous?
i think there are several problems, the biggest of which is imho: there’s no clear relation between the function and its param struct
sure, you can create fn foo
and struct FooParams
, but you’ll still have to define both separately, users have to import both separately, and you have to spell it out.
use fooer::{foo,FooParams};
foo(FooParams {...});
just doesn’t have the same simplicity as
use fooer::foo;
foo();
Agreed, both foo(FooParams {...})
and OpenBuilder().this().that().whatever()
look like hacks around the fact that there's no support for optional/default arguments.
Agreed, both foo(FooParams {...}) and OpenBuilder().this().that().whatever() look like hacks around the fact that there's no support for optional/default arguments.
You could look at this from different direction where languages are not expressive enough to express the builder pattern in a comfortable way have kwargs to gloss over that.
I personally really enjoy using the builder pattern most of the time (although it does have some quirks regarding finalizers) and think it is strictly more expressive (you can’t express e.g. mutually exclusive options with kwargs).
I find the builder patterns very nice to work with too.
I think Builder is the most ugly thing I've ever seen. First time I thought it's some piece of brainfuck code.
you know what? why not combine this stuff?
add a trait Params: Default
and syntactic sugar for invoking functions that have a Params
-implementing last parameter.
also add syntactic sugar for automatically defining an anonymous params struct.
e.g.:
#[derive(Params,Default)]
struct ExplicitParams {
herp: u8,
derp: i8,
}
//use when you have many parameters
fn explicit(durr: &str, named: ExplicitParams) { ... }
//use when you only have few parameters
fn implicit(foo: &str = "xy", bar = 1u8) { ... }
fn main() {
explicit("unnamed", derp = 2);
//desugared: explicit("unnamed", ExplicitParams { derp: 2, ..Default::default() })
implicit(bar = 2);
}
I'm pretty new to rust, so I haven't built complex macros, but I've seen others build really complex and powerful macros (even making a python like list comprehension in rust!)
Is this something macros can already do and, if not, isn't something like default/keyword args really in the domain of macros? It seems like the exact sort of thing macros are intended to handle.
If it were macros, it would be great to have an easy way to declare it, maybe something like:
#[kwargs(y:5, z:MyStruct{x: 4, y: 7})]
fn myfunc(x: u32, y: u32, z: MyStruct) -> u32 { ... }
This would wrap myfunc
with a macro. So you could still call the regular function:
let z = myfunc(1, 2, MyStruct{x:3, y:85}) // must specify all values
or you could call the macro/kwarg version:
let z = myfunc!(1, y:72) // don't need to specify y or z, y overridden here
// same as:
// let z = myfunc!(x:1, y:72)
This seems like the easiest way to be orthogonal to other rust features.
Edit: replaced =
with :
since =
returns ()
in rust.
@e-oz
I think Builder is the most ugly thing I've ever seen. First time I thought it's some piece of brainfuck code.
I encourage you to read the Rust Code of Conduct, in particular these two lines:
- Please be kind and courteous. There's no need to be mean or rude.
- Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
This comment does neither of these things. Please be respectful to others.
@vitiral that is my opinion, so please follow second rule from quotation.
Is this something macros can already do and, if not, isn't something like default/keyword args really in the domain of macros? It seems like the exact sort of thing macros are intended to handle.
i don’t think so. i think
when i see a macro invocation, i know that more is going on than a simple function call, and there are mostly only few macros. if you now couldn’t learn the 1-4 macros of a library anymore because every function became one, that would be bad.
What I am seeking for is the Qt binding in rust (mainly Qt widget rather than qml). It seems that such thing won't happen unless this issue is solved. Obviously the builder/macro would be too burdensome for this application. IMO a language cannot wrap Qt is not very "multi-paradigm".
when i see a macro invocation, i know that more is going on than a simple function call
I guess that was my whole point - kwargs are _always_ going to have more going on than a function call, aren't they? The creation of variables you don't see is going to take up more bytes in the binary certainly. I have a hard time believing they are ever a "zero cost abstraction", except in the case where the function is inlined.
Also, it would only "increase the number of macro creations / invocations" _when you wanted kwargs_. In my opinion kwargs are little more than convenience (and coming from python, they are VERY convenient), which is what macros are supposed to do.
As for static analysis, I think an analyzer could follow all uses that use the kwargs attribute I suggested, certainly.
@zhou13 why would macros be too burdensome?
One of the benefits of macros is that they let your kwargs have arbitrary code, so you can use constructors, etc for them. For example:
#[kwargs(y: 5, z: MyStruct::new!(4))] // `new` can even use kwargs itself! In this case `y: 7`
fn myfunc(x: u32, y: u32, z: MyStruct) -> u32 { ... }
This is a much more ergonomic way to declare kwargs I would think, and I think it would probably be a mistake to allow this kind of behavior in non-macros.
Edit: This gets more significant when you have a "default struct" that you want for one of your values. Ideally, you could just use a reference to a global variable for the default, but there might be cases where you can't. For example, suppose I wanted to make a read
function for a socket where you can pass in your own buffer to conserve on memory allocations
fn read(socket: &mut Socket, size: u32, buf: &mut Vec<u8>)
the buf
variable is there to give the user an optional performance boost, but you don't want the user to _need_ to include it if they don't care that much about performance. Therefore you declare the kwargs:
#[kwargs(buf: &mut vec!())]
This gives your api that flexibility.
Doing arbitrary code without macros sounds non-rusty to me, so doing kwargs any other way than macros while having this ability is not possible.
The creation of variables you don't see is going to take up more bytes in the binary certainly
not more than adding code to it like let arg_val = match opt_arg { Some(v) => v, None = default };
you also can’t reference macros, and they’ll have no connection to “their” function other than the name.
sorry, i don’t think this is a good substitution at all.
@flying-sheep What do you mean "you can't reference macros"?
well, you can create references to functions
@flying-sheep Ah, I see what you mean.
This is actually a very good question -- would it be even possible to have zero-cost abstractions for function pointers/references with kwargs?
I ask, because the problem seems non trivial to me. Say you have fn A(x: u8, y=18: u16)
and fn B(x: u8, y=49: u16)
. If you have a function with function type fn(u8, u16)
, you simply wouldn't be able to use the kwarg functionality. How would the compiler know which value you want for u16
when you can take either A
or B
at runtime?
You might be able to write the function type to be fn(x: u8, y=18: u16)
, and then this would only be able to accept A
but not B
, and you wold be able to use kwarg functionality -- but this seems very limiting and not all that useful.
Without this restriction though, I don't think using kwargs with function pointers is even possible without incurring a cost (like carying around the default value with every kwarg function, forcing every function call to check if the function is a kwarg function to apply defaults, etc). Again, the macro makes it explicit that you don't care about the extra cost and you just want the convienience.
the macro option also has the benefit that it doesn't create two classes of functions, functions that have at least one kwarg implementation for them and functions that don't. There will always be a runtime penalty if a function could potentially take kwargs (i.e. if you had fn C(x: u8, y: u16)
and then you defined A
or B
, using references to C
would now be slower since the compiler would have to check whether there were kwargs).
A macro is clearly separate from a function, so it does not create this separation.
why would macros be too burdensome?
@vitiral Because Qt heavily uses optional args and function overload. If we want to use macro, then you almost need to add !
to the end of every basic function. More importantly, I think you cannot use macro in trait methods.
Actually it looks like you cannot even use macros as parts of crates or modules. I.e. I cannot write
use mylib;
mylib::!mymacro()
This is problem for my proposal (and actually a problem with macros, I'm curious to know why the devs chose this). I did not realize the full extent of this until now.
The fact that it is not easy to import and use macros seems like a problem with macros. I personally would like macros to be scoped like everything else, and don't see why that's not possible in theory.
Without scoped macros, though, obviously using macros for kwargs doesn't make sense
This is problem for my proposal (and actually a problem with macros, I'm curious to know why the devs chose this). I did not realize the full extent of this until now.
Because macro expansion happens early in the compilation process.
I would be interested in keyword-based / default parameters. One concern I have seen raised often is the fact that the parameter names become part of the API and thus changing it becomes a breaking change. Keyword-parameters should therefore be opt-in.
I am not sure about what designs have been proposed thus far, I skimmed through this issue and an old rfc but this is one idea I had:
// Normal function with positional parameters
pub fn positional(a: i32, b: i32) -> i32;
// Function with keyword parameters
pub fn keyword(pub a: i32, pub b: i32) -> i32;
// Function with default keyword parameters
pub fn default_keyword(pub a: i32 = 0, pub b: i32 = 2) -> i32;
// Mix of all
pub fn mixed(a: i32, pub b: i32, pub c: i32 = 0)
pub
is already a keyword. Using it here would totally coincide with it's meaning: "making something public / part of the API". The use of pub
would make it very clear that this parameter is facing the outside world, making it part of the API and everything that that implies.
I thought I would write it down here so that others could share their opinions about this idea :)
Concerning the builder pattern, I think it is not a bad pattern. It definitely has it's use cases and they certainly do partly overlap with default / keyword parameters. But I don't think that should be a reason to exclude keyword parameters. When they fit on one line they are far less bulky than the builder pattern. In my opinion, lib authors could even propose both at the same time!
Wow @azerupi that's a fantastic idea for the syntax. I really like it.
@azerupi That's a solid suggestion!
@azerupi, nice idea. My only worry is that this syntax cannot be extended to a future RFC about default values for struct fields.
Sorry, I am new here, so I can miss some nuances, but I really do not like such "pub" using - it makes signature more noisy. Yes, they can increase probability of breaking changes, but I doubt that parameter names will often change without changing their meaning in stable API. Besides, we have semantic versioning. And if you change behavior, then it will even be helpful to break compilation.
Additionally, it will be library authors' responsibility to choose between "stability" or convenience. I suppose library users can choose that for themselves. Moreover, library authors already can influence that by not creating functions with too many parameters, so users will not be tempted to use named arguments without reason. (:
Additionally, it will be library authors' responsibility to choose between "stability" or convenience. I suppose library users can choose that for themselves.
@DarkEld3r With enough users, somebody will end up relying on it. Then when you decide that a more expressive name would be nice, it becomes a breaking change.
If it is exposed, it becomes part of the API. Library authors bear the responsibility of tracking breaking changes and alerting users when they occur, so library authors should have the ability to limit what they expose.
If it is exposed, it becomes part of the API. Library authors bear the responsibility of tracking breaking changes and alerting users when they occur, so library authors should have the ability to limit what they expose.
I don't really understand what's the big fuss about exposing keyword argument names. The function name itself is already 'exposed' anyway, so changing it to a more expressive one would also break the API. Say, Python has had keyword/default arguments pretty much since the start and that's been a great help for library developers rather than the opposite.
If you as a library developer would want to rename a keyword argument in a nice way, you could always support both the old and the new name for one release cycle and use #[deprecated]
to trigger a compilation warning if an old name is used.
The function name itself is already 'exposed' anyway
And that was explicitly decided by using pub
before the function. My understanding of @DarkEld3r's comment was that every pub
function should have all of its parameter names be publicly available for use with keyword arguments. Fair point though; while this would add to the API a pub
function exposes (you used scare quotes around exposed; is there a better word?), it's not a big deal to make sure that you're as confident in your parameter names as you are in the function name and (most importantly) the types when you make it pub
.
Say, Python has had keyword/default arguments pretty much since the start and that's a great help for library developers rather than the opposite.
I feel like Rust has a somewhat different stance than Python. Rust makes some strong static guarantees, is often conservative in its defaults (e.g. immutability and privacy), and generally allows for a fair amount of control (e.g. pub
struct vs. pub
members1).
I suppose library users can choose [between "stability" or convenience] for themselves.
doesn't seem to fit with that. That said, I'm in no way opposed to either keyword or default arguments. I just want to make sure we're thinking things through.
If you as a library developer would want to rename a keyword argument in a nice way, you could always support both the old and the new name for one release cycle and use #[deprecated] to trigger a compilation warning if an old name is used.
How would that work?
1: This is different from keyword arguments because it exposes more than just names.
Here are some thoughts on each of these ideas:
Keyword-Based Parameters
The biggest problem with keyword-based parameters is that they make it easier to write functions that take a large number of parameters. Ruby on Rails is a good example, with a number of functions taking 5+ parameters. I think most Rust developers would agree that this is an anti-pattern.
It also makes parameter names part of the public interface of every public function. While this may be less onerous, it is still a surprising change. It is also a major change of expectations for existing Rust code. Even if it's not considered a breaking change itself, it may be undesirable from the perspective of a current Rust library maintainer.
Variable-Arity Functions
Rust has macros, which can already be used to implement this sort of thing. Rust has a solid track record of keeping features orthogonal, and any non-macro-based system for this would be a strictly less-useful system re-implementing an ability already effectively available in the language.
Optional/Default Parameters
I don't have a strong opinion on this one. It seems like it's a new complication for very little gain, but maybe there are good arguments for it. All the ones I've seen so far in here have been aesthetic, which isn't a particularly compelling angle.
I don't think named parameters are an anti pattern, unless you consider the builder pattern one too, which is a very similar use case. Having 5+ params in a function may just be bad design, or it may be needed.
Variable-Arity functions can be worked around by using vecs or similar, but still something that should be core of the language, rather than having to use macros for every new function you create.
Optional/Default Parameters are great at providing intent within the signature.
All these 3 features would be a great addition.
Sorry, I realize now the wording was ambiguous. I meant that having functions with a large number of parameters is an anti-pattern.
As far as variable argument function support, are you really needing them for every function?
Not every function, just ones where it makes sense and improves readability.
Yes you could use a macro, but each time you have a different function you would need to create a new macro to support it.
As I commented above, you really can't have anything _but_ a macro because otherwise there is no way to have initial variables that are structs or made through (for example) a new()
method. Having initial values is great, but kind of pointless without that ability (in my opinion).
The problem with macros is that they aren't well supported from a namespace standpoint, and so having a macro-per-kwarg-function is not acceptible.
Once macros are fixed to work inside namespaces though, I think that is the way to go here.
I've definitely wanted optional/default args. In most cases, it's "meh, good enough" to just have an extra function with a different name or allow None
or Default::default()
for the extra args, but in at least one case, I iterated over several solutions before deciding to take a single Into<AlgoRef>
arg and implemented the trait for both &str
and the tuple (&str, Into<Version>)
so I can call algo("foo/bar/0.1")
or algo(("foo/bar", Version::Minor(0,1)))
. It definitely feels hackish/unnatural to need the extra parentheses to accomplish this, but for the particular requirements, I preferred this trade-off (and have followed this issue ever since).
As for variable-arity, I haven't actually encountered a need, though I do think it'd be elegant if method-level macros existed to call things like logger.info!("foo: {}", foo)
- but I won't pretend to understand the implications.
It's probably easiest to leave variable-arity to macros, well they are used quite rarely.
I wonder if named arguments could be achieved with very ergonomic anonymous structs? You might call it like f({ .. })
because it's anonymous. I donno if named arguments are wise, but the extra { .. }
might help discurage their abuse.
I think care should be taken with default arguments since they could be used by mistake. A priori, I'd suggest the programmer to writes a placeholder like f(a, , b)
or f(a, !, b)
.. I suppose f(a, _, b)
sucks because _
might denote lambda.
Another option is simplify giving default parameters a specific name, so maybe an enum but probably a macro or inlined, and maybe scoped to the function. It offers the advantage that multiple defaults can be specified, but abusing defaults is discouraged.
It's absolutely doesn't matter what is anti-pattern and what is not.
Programming language is a tool, not a vice squad. Yes, I don't like
functions with many arguments, but sometimes we need it. I also don't like
"unwrap", but sometimes it's needed. If something is just case of taste -
it's not an argument.
On Fri, 25 Mar 2016 at 14:29 Jeff Burdges [email protected] wrote:
It's probably easiest to leave variable-arity to macros, well they are
used quite rarely.I wonder if named arguments could be achieved with very ergonomic
anonymous structs? You might call it like f({ .. }) because it's
anonymous. I donno if named arguments are wise, but the extra { .. }
might help discurage their abuse.I think care should be taken with default arguments since they could be
used by mistake. A priori, I'd suggest the programmer to writes a
placeholder like f(a, , b) or f(a, !, b).. I suppose f(a, _, b) sucks
because _ might denote lambda.Another option is simplify giving default parameters a specific name, so
maybe an enum but probably a macro or inlined, and maybe scoped to the
function. It offers the advantage that multiple defaults can be specified,
but abusing defaults is discouraged.—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/rust-lang/rfcs/issues/323#issuecomment-201245439
Variable Arity and named parameters are also great predecessors to more FP stuff like currying.
It sounds like everyone is opposed to functions that do too much, and by extension, functions that may have way too many args, but I don't think that keyword/default args or variable arity should be on the chopping block because they could be misused.
Many argued strongly in favour of using structs, and I'm using structs as args extensively in a library I'm working on; but, that's because the data should be grouped together like position: Xyz{x: f64, y: f64, z: f64}
. However, this is a far cry from being a perfect solution in all cases.
To address @AndrewBrinker,
Variable-Arity Functions
Rust has macros, which can already be used to implement this sort of thing. Rust has a solid track record of keeping features orthogonal, and any non-macro-based system for this would be a strictly less-useful system re-implementing an ability already effectively available in the language.
I am strongly in favour of re-using existing tools if possible, but only if they're a good match for the job. I'm not even sure if macro's could solve the following problem, regardless their documentation doesn't make them look trivial to use/understand/maintain (and I'm no stranger to meta-programming and manipulating an AST for codegen). Whereas in the following code with variable-arity functions allowed, it's trivial for even a novice to understand what's happening.
After doing a bunch of programming in Erlang/Elixir, I really miss the ergonomics of variable arity/default arg functions at times like below:
pub struct SceneGraph<'a> {
pub tree: RefCell<RoseTree<GraphNode<'a>>>,
root: GraphNode<'a>,
}
impl<'a> SceneGraph<'a> {
pub fn add_node(&mut self, parent: NodeIndex, node: GraphNode<'a>) -> NodeIndex {
......//Extra logic removed
self.tree.borrow_mut().add_node(parent, node)
}
pub fn add_node(&mut self, node: GraphNode<'a>) -> NodeIndex {
self.add_node(self.root, node)
}
}
A: add_node/3
, where node: Option<GraphNode<'a>>
B: rename add_node/2
to add_node_root/2
.
C: if default args are powerful enough, they could do self.root
; But, let's imagine that we need to do some computation or something beyond the ability of a default arg.
If I understand correctly, A would carry a runtime penalty to unwrap and test.
B entails an additional method name/ergonomic issues to deal with when using the library. Alternative B also embeds unnecessary implementation details in its public interface, this poses a challenge if an internal implementation detail changes**, and then the method no longer accurately reflects its purpose, prompting me to rename it to add_node_wubba_lubba_dub_dub/2
. For the sake of this discussion, please don't get pedantic about this example, and focus on the spirit of the use-case (It can be useful for multiple functions to share the same name, because externally they do the same thing, and your public interface shouldn't leak implementation details.)
**With no change to the end user because :heart: semver.
If this gets supported, +1 for @azerupi's syntax suggestion, repeated below (although it might need some refinement to also work with variable-arity; Erlang/Elixir are good examples of doing both variable arity and default args).
Keyword args/defaults should be opt-in on a per variable basis, for the reasons @reddraggone9 mentioned.
// Mix of all
pub fn mixed(a: i32, pub b: i32, pub c: i32 = 0)
// Variable Arity
pub fn mixed(a: i32 = 0)
Would create functions mixed/0
, mixed/1
, mixed/2
, mixed/3
. Where mixed/0,1
invoke the latter definition, while mixed/2,3
invoke the former. These need to be mutually exclusive sets, unless we want ambiguous function choices from the compiler a la Java et al, which IMO should be a compiler error.
Note: NAME/0
- Denotes a function called NAME
with arity 0 (Takes 0 arguments).
Can't a compiler just to reorder keyword arguments and fill an omitted arguments places with a default values (or raise an error when an argument does not have a default value and is not specified)? Consider the following function:
fn foo(a: i32, b: i32, c: i32 = 3, d: i32 = 4)
A programmer may call it using:
foo(1, 2, 24, 71)
- just like it works right nowfoo(5, 6, c: 17)
- gets translated into foo(5, 6, 17, 4)
foo(5, 6, d: 48)
- gets translated into foo(5, 6, 3, 48)
foo(5, 6)
- gets translated into foo(5, 6, 3, 4)
foo(b: 35, a: 18)
- gets translated into foo(18, 35)
, which, in turn, gets translated into foo(18, 35, 3, 4)
foo(b: 35)
- throws a compile error, because a
is not specified and does not have a default valueI believe, this will not have an impact on a runtime performance, because arguments reordering and stuff will be performed during a compilation.
@zmoshansky How do you propose handling a case like this:
pub fn add_node(&mut self, node=GraphNode.origin(): GraphNode<'a>) -> NodeIndex {}
Clearly (to me at least) you cannot do this in anything but a macro -- you are _calling a method_ in the declaration.
@vitiral, Thanks for the feedback. As I understand it (GraphNode.origin()
being GraphNode::origin()
), this would be easiest to do with variable arity functions. By defining a add_node/1
this could be easily achieved as follows. Essentially add_node/1
would call on add_node/2
which would call upon add_node/3
.
pub fn add_node(&mut self) -> NodeIndex {
self.add_node(GraphNode::origin())
}
I think variable arity is particularly useful in cases like this where the default is too complex/not able to be done at compile time.
@vitiral You can just restrict the default values to whatever is assignable to const
, including const fn
, problem solved.
@e-oz, encouraging antipatterns by providing fancy syntax for it is not something that is aligned with Rust's design philosophy.
@zmoshansky ah, I see. I see now that variable arity was one of the options given in the thread title - I was pretty focused on kwargs.
Variable arity is certainly possible. I'm honestly not sure why it's not implemented already since it so simple. I could see it causing problems with function documentation (since you now have multiple functions), as well as lending a hand to making confusing APIs. It might help in some places though.
@aldanor I would see that as of pretty low usefulness, but maybe that is just me. Having complex structs with new
defined, but not actually being able to pass any variables in would get annoying. You would have to reimplement new
for every value.
Coming from python it seemed at first glance like this would be really useful for dynamic string formatting (#642)
let template = String::from(".. {id} .. {id} .. {id} ..");
template.format(id="foo");
... but that won't actually work. In python keywords are just hashmaps which seems like a nonstarter for rust.
Regarding keywords this changes my previously tacit :+1: to :-1:, unless I misunderstood and keywords aren't supposed to be just syntactic compile time checked sugar for positional arguments.
Coming from Python, too, I'd say I prefer using keyword-only arguments whenever possible because it reduces ambiguity and prevents from shooting yourself in the foot by accidentally mixing up the positional args and keyword args.
With this in mind, maybe this could work:
&mut
Example:
fn foo(x: u32, y: u32 = 0, z: u32) -> { () } // ~ERROR positional arg after a keyword arg
fn bar(x: u32, y: u32 = 0) -> { () } // ok
fn main() {
bar(1, 2); // ~ERROR `y` is a keyword argument
bar(x=1, y=2); // ~ERROR `x` is a positional argument
bar(1, y=2); // ok
}
While this may look overly simple, I believe it would be sufficient to cover the cases that currently require using the builder pattern + it's fairly unambiguous to read or parse.
@aldanor Could you please explain the rationale behind your proposed restrictions? I worry that it sacrifices a lot of the flexibility of this proposed feature, for an unknown benefit.
I do like the idea that positional args must be defined before keyword arguments; Although, it's not a technical requirement AFAIK.
I strongly oppose that keyword arguments must have a default value, and to use a default value, you must use a keyword argument. Especially given the call site requirements you've outlined.
As an aside, I've opened a new RFC #1586, to deal with variable arity functions as this thread seems focused primarily on kwargs & defaults.
@zmoshansky The rationale is that I think this is a less intrusive approach, easier to implement, and it's possible to later extend it if that's ever needed. I believe this fully addresses the pain of having to write Builders in cases where multiple optional parameters (that always have defaults) are needed.
I strongly oppose that keyword arguments must have a default value, and to use a default value, you must use a keyword argument.
Could you provide concrete examples where any of the proposed restrictions would impose a _functional_ restriction on your code? (syntactic sugar aside)
@aldanor, Thanks for that thoughtful reply. I agree that this provides a nice alternative to the builder pattern, which I'm not a fan of being forced to use (Time and a Place for everything). My feedback was directed at the broader use of these features, outlined with reasoning below.
I don't believe that coupling these two features would impose any _functional_ restrictions, although I feel that the entirety of this RFC is really about non-functional syntactic sugar. When I say that I mean that everything _could_ be implemented with zero runtime overhead in a different manner (although potentially quite ugly).
To clarify, I strongly oppose the tight coupling of keyword and default arguments, where one is impossible without the other. In my personal opinion, I feel that this tight coupling of related features violates Single Responsibility
and Low Cohesion
principles and would actually make the implementation less robust. Even if the usage of keyword arguments
subsumes default arguments
to be practical, I think that it's a fallacy to assume the default arguments necessitate keyword arguments
. I do understand that if a positional argument has a default argument, then it might be nice to mandate that a positional arg with a default value must be after all of the positional arguments without default values.
I feel that they both have great merits, and for instance, I would love to use default arguments
with positional arguments
. This may be a moot point if the envisioned call syntax using a default arg is as follows bar(1);
(where y=0), but it feels inconsistent if I then want to do bar(1, y=2)
.
I started playing around with some options last night. Partially inspired from Racket, and ML. I think something like this could work well. The main idea is that there is a correspondence between our current function definition syntax and tuples, so named arguments should correspond to struct syntax. Then the task of adding in default arguments becomes a bit simpler. The big concern with this idea is that it's too big of an addition to the existing syntax, and if it's even backward compatible at all. I haven't thought about that enough yet.
Here's some examples.
// 1. Tuple style method call (current functions).
fn foo(a: u32) {}
foo(2);
// 2. Struct style method call.
fn foo { a: u32 }
foo { a: 2 };
// SomeType { a: 1 } -> SomeType
// SomeFunc { a: 1 } -> Codomain
// generally_lowercase { a: 1 } -> Codomain
// 3. Together.
fn foo(a: u32) { a: u32 } {}
foo(1) { a: 2 };
This syntax for function decs/calls is consistent, and should work well with both optional and default arguments.
// 1.
fn foo(a: u32 = 1) {} // Using const function.
fn foo(a: u32) {} // Using `Default` trait.
foo();
// 2.
fn foo { a: u32 = 1 } {} // Using const function.
fn foo { a: u32 } {} // Using const function and `Default`.
foo {};
// 3.
fn foo(a: u32 = 1) { b: u32 = 1 } {} // Using const function.
fn foo(a: u32) { b: u32 } {} // Using const function and `Default`.
foo();
foo {};
foo(){}; // We probably want this for consistency (macros).
In practice I can see the named argument syntax getting very long, and people wanting to do things like this:
fn foo(...) {
arg1: u32,
arg2: u32,
} {
...
}
This is obviously not good. We could then alternativly use an #[arg(...)]
or #[named_arg(...)]
attribute. This depends on https://github.com/rust-lang/rust/issues/15701 for function call attributes.
There would be a decision of whether or not we want to allow all three types of args in attributes or just named arguments. Below are some examples with all three.
// Declaration.
#[arg(a: u32)];
#[named_arg(b: u32)];
#[ret(u32)]
fn foo {
unimplemented!()
}
// ==
fn foo(a: u32) { b: u32 } -> u32 {
unimplemented!()
}
// Call.
#[arg(a = 1)];
#[named_arg(b = 2)];
foo();
// ==
#[arg(a = 1)];
#[named_arg(b = 2)];
foo {};
// ==
#[arg(a = 1)];
#[named_arg(b = 2)];
foo(){};
// ==
foo(1) { b: 2 };
// Example with defaults.
/////////////////////////
impl Channel {
#[arg(timeout: Duration = Duration::from_millis(10))];
#[arg(pre: Option<Fn(D) -> bool>)];
#[arg(post: Option<Fn(C) -> bool>)];
fn call<D, C>(domain: D) -> C {
unimplemented!()
}
}
#[arg(timeout = Duration::from_millis(20))]
channel.call(message);
#[arg(timeout = Duration::from_millis(20))]
#[arg(pre = |d| d > 10)]
#[arg(post = |c| !c.is_empty())]
channel.call(message);
I have some concerns with using Default
implicitly, as it could make it very easy to forget an argument and then have the program do _something_. One of the best things about rust is how I can use the type signatures to drive my development.
Last we could further alleviate issues describing function signatures with a new sig
keyword. I'm not necessarily recommending this, mostly just exploring options.
// Signature declaration.
sig foo(a: u32) -> u32 {
b: u32,
}
// Function declaration.
fun foo {
unimplemented!()
}
// Call.
foo(1) { b: 2 };
That's the best proposal I've seen so far, @nixpulvis. I agree that the use of Default
is worrisome, since having function signatures be explicit is such a nice benefit for understanding code, especially code you may not already be familiar with. I don't like the use of attributes for anything related to these features, however. Attributes make more sense to me as a special instruction to the compiler about the item. If there are other non-signature related attributes, it may be quite difficult to read and understand exactly what the signature is. If the signature (sans-attributes) being verbose is something that people find unappealing, perhaps that would be a deterrent for having a signature with a very large number of options and giving one function too much responsibility.
I used to think that optional/default values would have to be constants (or immutable statics) to be feasible.
However, @zmoshansky's https://github.com/rust-lang/rfcs/pull/1587, even if it got closed, provides another interesting way to provide default values.
My main critique of https://github.com/rust-lang/rfcs/pull/1587 was that it used to allow arbitrary overloading by arity, which I found confusing. (I generally don't like ad-hoc-overloading, it feels hacky and unsystematic.) But with some tweaks (some of which were provided by the last commit in the RFC), the ad-hocness disappears.
Simply put, there is the "full" form of our n-ary function that contains all the n parameters, for example fn example(a: i32, b: i64, c: u8)
, but you can also define "shortened forms": fn example(a: i32, b: i64)
and/or fn example(a: i32)
and/or fn example()
. The forms form a set so that the names are the same but arities are different and mutually exclusive, and the types and parameter names in the common prefixes are always the same, like in https://github.com/rust-lang/rfcs/pull/1587.
What would be different from https://github.com/rust-lang/rfcs/pull/1587 is that the shortened forms are required to tail-call some other form that has larger arity than the caller, for example fn example(a: i32)
would have to tail-call fn example(a: i32, b: i64)
or fn example(a: i32, b: i64, c: u8)
. This ensures that the body of the full form is always eventually called, and the shortened forms then function basically as default-value set-upper shims. This has the benefit that the default values don't have to be constants. (Useful, for example, in a case where the default value can be inferred from some other parameter in case no value is provided by caller.)
If the default values were bound to be constants in cases where constants simply don't cut it, you'd have to use Option<T>
and set None
as the default value, which requires unwrapping and setting up the default value inside the function, which is kinda icky. I'd imagine a shim-style set-upper would generate more efficient code too.
This is just an idea, it would still require thinking about the fully qualified paths, and the interactions with kwargs (with a default-valued kwargs, the shims would not form a total order but some kind of a lattice), plus revive the discussion about tail-calling.
Ah, one more thing, about default values using the Default
trait: I find this to be problematic, because default()
builds the whole struct, even if just a single default value of many is being needed. Building the default values might have side-effects so it might be hard to optimize away the construction and dropping of unused default values.
I think this topic is really important because it impacts significantly Rust libraries public API, forcing developers to use some workaround that make their API less clean and less elegant.
While I understand why the 3 proposals has been grouped together, my concern is that there is too much here to make significant progress and move forward. So my proposal would be to focus on improving Rust current capabilities instead of adding multiple alternatives for doing the same thing that would be confusing for the developer, make the compiler more complex/slow and impact runtime performances.
So why not focusing on improving position-based parameter rather than introducing something new that could be confusing for users (should I use position-based or keyword-based parameters ...)?
For position-based parameters, I think the optional parameters proposal is great because simple, it is an improvement in line with current Rust mindset, and would be VERY useful to design clean APIs. I don't think variable-arity would add much more benefits.
So in practice, reusing @orgkhnargh example, for we could only define one foo
function, but have the capability to specify optional parameters with default constant values:
fn foo(a: i32, b: i32, c: i32 = 3, d: i32 = 4)
A programmer may call it using:
foo(1, 2)
foo(1, 2, 24)
foo(1, 2, 24, 71)
That's all.
Such change would be useful for all kind of applications IMO, and would especially allow more wider adoption for Rust for developing high performance Rust web applications. That could also help making Rust a good alternative to Go or Swift 3 for server-side applications (when high-performance is needed, Reactive applications could also be a good use case).
So I hope a RFC focusing on optional parameters with default values will be created/reopened with higher priority, and this one updated to keep discussing with a postponed status only about keyword-based parameters and variable-arity. Developers feedback after optional parameters introduced in Rust could provide great insights to help deciding what to do on those 2 remaining proposals.
My problem with many of the proposals here, is that they encourage an anti-pattern, namely having too many arguments, where grouping it into a simple structure would be considered better pratice. In many of the cases made in this thread, either builder pattern or a struct would be sufficient.
I agree that with named-parameters there is a big risk for too many arguments, but I think the risk is much lower with optional parameters. Optional parameters with default values allow you, as an API designer, to give the possibility to the user to provide less parameter if he wants to rely on the defaults your have defined while allowing to make your API self-documented about these default values (and documentation can obviously reuse that automatically). But unlike named-parameter, you have more constraints due to the order of arguments, since if you specify an optional parameters the previous ones must be specified.
What I have seen in languages supporting such feature is that this is widely used, usually in a good way. That seems to me a good trade off.
My problem with many of the proposals here, is that they encourage an anti-pattern, namely having too many arguments, where grouping it into a simple structure would be considered better pratice. In many of the cases made in this thread, either builder pattern or a struct would be sufficient.
... that is, unless you consider the builder pattern to be an anti-pattern on its own. Being forced to create an alien builder struct just so you can have a ctor that takes 1, 2 or 3 arguments is more of an anti-pattern in my opinion than having a ctor with two optional arguments with defaults. Sure, if you have more than one function expecting the same set of arguments, it calls for the builder pattern, but that doesn't happen too often, not even in the standard library.
Another point which may have not been raised here is that the generated docs may end up being easier to read if the default/optional args are used instead of builders -- e.g., it's a lot nicer to be able to see the full signature of a ctor on the relevant struct's page where it belongs as opposed to having to read the separate page for the associated builder.
I agree with @aldanor. To me the builder pattern feels worse than keyword arguments. Same for exporting an option struct.
Let's take an extreme example using python: http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html
For some people that would be a very bad function, 45 args! You could try breaking it into function that takes at most 2-3 arguments but good luck with that.
Builder pattern is out, not going to write 44 methods and it just feels wrong (more on that later). Option struct and kwargs are left.
Let's take a simple CSV reading example:
df = pandas.read_csv('data.csv', encoding='latin1', low_memory=False)
Here are the options in Rust:
// option struct
let options = CSVOptions {
filename: "data.csv",
encoding: "latin1", // could be an enum
low_memory: false,
};
let df = pandas::read_csv(options);
// kwargs
let df = pandas::read_csv('data.csv', encoding='latin1', low_memory=false);
The kwargs option is in my opinion much clearer because the library doesn't have to export a specific struct for each function which would be a horrible solution. In my opinion kwargs make libraries API much nicer and is something that I seriously miss in Rust.
Let's take a Rust example from glium:
// builder pattern
let display = WindowBuilder::new()
.with_dimensions(1024, 768)
.with_title(format!("Hello world"))
.build_glium()
.unwrap();
// kwargs
let display = WindowBuilder::new(
dimensions=(1024, 768),
title=format!("Hello world"),
).unwrap();
I have a hard time understanding why people would prefer the first one since you would need a new builder for every function taking optional arguments.
If anyone preferring the builder pattern can chime in, I would appreciate it!
I personally love @azerupi proposal but wouldn't it be better to separate the kwargs/default args (I feel like you need both to do nice APIs) and variable arity into two different issues?
Not sure if this has been suggested before, but another option is to hang the function call off the options struct directly:
pandas::ReadCSV { filename: "data.csv", encoding: "latin1", low_memory: false }.call()
If https://github.com/rust-lang/rust/issues/29625 is ever stabilized it could also be:
pandas::ReadCSV { "filename: data.csv", encoding: "latin1", low_memory: false }()
@Keats for most people it isn't really about whether we like kwargs, it's the technical challenge of fitting it into rust without it causing overhead.
@glaebhoerl that would be fine for some use cases, but doesn't address default args (which is 95% of the point of kwargs IMO).
If you had default struct args you _could_ be part of the way there, but that still doesn't address default non-static types. I honestly don't know if that can ever be addressed. Some possibilities:
lazy_static!
variables so that kwargs can use them as well[]
as a default is a PITA)Maybe lazy_static!
already does what you need it to do and all of this could be done for any type. That would certainly be nice.
I would like to still point out that you would have to completely throw away the kwargs when you are using the functions as a function pointer, as holding that data would drastically increase memory and performance costs.
One of the reasons I like @glaebhoerl 's suggestion (combined with mine) is that it solves kwargs in two spheres: Struct and Fn, but would not add additional types of Fn
s.
For keyword arguments, @flying-sheep's trait + sugar based proposal seems ideal to me (https://github.com/rust-lang/rfcs/issues/323#issuecomment-1651847030), the only issue being @vitiral's comment about the need to support what could essentially be described as runtime in addition to compile time default argument value instantiation (https://github.com/rust-lang/rfcs/issues/323#issuecomment-178053176).
I just wanted to point out that this doesn't really seem like it should actually be a problem -- wouldn't you just have the default value be None
, and then manually instantiate the actual default value in the function body? (In which case, maybe the desugaring should convert value
to Some(value)
for any argument struct member with Option
type?)
After reading the whole conversation I clearly see that all three issues here should be discussed/implemented separately. Especially It's the case for variable airity. It already has been stated a few times here, but clearly those issues are still mixed up here and there.
From my point of view keyword syntax for argument passing and default values are good things to have (less boilerplate, cleaner look, better readability).
Personally I'd prefer @flying-sheep's proposal, but the one from @azerupi looks good for me too.
I have started a pre-RFC discussion on the internals forum about named parameters: https://internals.rust-lang.org/t/pre-rfc-named-arguments/3831
you forgot to mention my proposal under the alternatives. is this intentional?
Not intentional :confused:
I definitely should mention it, as it is quite a good alternative. I will update the proposal. Thanks for remembering me :)
Edit: It's updated: https://internals.rust-lang.org/t/pre-rfc-named-arguments/3831/62
thanks!
I'm referencing #1806 , as I believe it has a potential impact on the future of these features.
Forgive any ignorance about the builder pattern as I'm new to the language, but how does it help if you're just trying to write a trait? Would you make the trait such that it exposes a Builder API?
Would really like to have this feature as well. Am new to Rust, and I'm surprised it doesn't have such a convenience feature.
I'm doing Python at work, not a fan of it, but it has both argument mapping by name and default argument values and, oh, do I miss those when I code in Rust!
They just released the 2018 roadmap with no mention of default argument values or named arguments https://blog.rust-lang.org/2018/03/12/roadmap.html 😢
Maybe add function overload over arity?
fn foo(a: u32) -> u32 {
a + 5
}
fn foo(a: u32, b: u32) -> u32 {
a + b
}
fn main() {
let _1 = foo(1); // 6
let _2 = foo(1, 3); // 4
}
@sergeysova I predict that different arity will instead happen via variadic generics and traits.
Using C++-like syntax as a placeholder, that could mean something like
trait Foo<T...> { fn foo(args...: T...) -> Self; }
impl Foo<u32> for u32 { fn foo(a: u32) -> u32 { a + 5 } }
impl Foo<u32, u32> for u32 { fn foo(a: u32, b: u32) -> u32 { a + b } }
fn foo<T..., R>(args...: T...) -> R { <R as Foo<T...>>::foo(args...) }
(aka basically the same pattern by which "overloaded" things like Vec::get
work.)
Over in #2443, @Ixrec has asked me why I feel critical about named/optional arguments. @steveklabnik suggested I reply here so that's what I do.
There are a few reasons:
None
this is visible. Right now when I want to pass some arguments to a function, I know that I either have to change the function and all of its invocations, or I have to look for alternatives. With optional arguments, I'd have to look at the function declaration.system.open_valve(id, 1.00, false)
is pretty cryptic while system.open_valve(id, opening_speed=1.00, emit_event_when_open=false)
reads much better. However, I feel that this readability increase is better done by IDE tooling (like here: https://youtu.be/ZfYOddEmaRw).sad news...
Point 3 is funny enough, I've never been thinking about new things and improvements under this angle. "What if it will be too attractive and people will rewrite old things?", lol.
Re point 3, this is already happening with async IO and futures, no need to worry about something that already exists. In fact, I'd argue named arguments (as an optional feature) is more compatible than trying to get futures to work with other libraries.
Re point 4, I'm glad your IDE works well, but that is not the case for all editors people use with rust, and implying that the support there is good enough to not improve the language legibility simply sounds like you lucked out on which editor you prefer.
Re point 6, is there anything actually stopping people from breaking semver in crates?
You need to write a lot of additional boilerplate code for builder pattern and it only usable when ALL arguments are optional, because you can't force API user to call builder methods. It's not always the case, obviously, often functions have required arguments.
Well, it's enough just to check history of this issue to lose any hope about this feature. Rust is not good enough to get this feature.
Re point 3, this is already happening with async IO and futures
Sunken cost fallacy. Also, with async IO there is a strong technical reason for doing it, it's not done for fun :).
Re point 4, I'm glad your IDE works well, but that is not the case for all editors people use with rust
Definitely a legitimate counter argument. But I still feel that the desire whether you want to see argument names or not is subjective and depends on the situation and thus is managed flexibly by a tool and not put into persistent source code.
- There is a readability benefit of named arguments.
system.open_valve(id, 1.00, false)
is pretty cryptic whilesystem.open_valve(id, opening_speed=1.00, emit_event_when_open=false)
reads much better. However, I feel that this readability increase is better done by IDE tooling
Not committing to an opinion overall, but I want to point out that the argument about IDEs does not capture the whole story at all.
This is important as a lot of the interaction developers have is with code that is not inside their IDE. Readability is really important — for a text-based language, it should not depend on the tools.
That said, the other points are useful points. (Though I think that, in the case of optional arguments, you need to explicitly take into consideration the fact we already have default generic parameters, when you state your case, because there's a consistency argument to be made there.)
Edit: Just saw similar replies re. editor integration, but I think it's important to point out it's not just about _editors_ per se.
I feel that the sunken cost fallacy is different to this situation in some respects. For it to be a sunken cost fallacy, we'd have to have already paid the cost and want to double down to achieve our goal.
Futures isn't stable yet, and until it is there is still an upcoming requirement for rewriting many if not most crates to be compatible with it. So the cost isn't paid yet in terms of the crates ecosystem. This IMO gives us an opportunity to add features that would prompt rewrites before that happens, so that we only get one massive rewrite-pocolype not multiple ones.
@e-oz It's possible to force required arguments with the builder pattern.
The most obvious is to put the required arguments in the new
function:
MyBuilder::new(required_param1, required_param2)
@Pauan it's not part of the builder. With same success you can call setters "a pattern builder".
@e-oz It is absolutely a part of the builder pattern. The builder pattern has three parts: the initialization, the method calls, and the finalizer:
MyBuilder::new(required_param1, required_param2)
.some_method(optional_param)
.done()
This gives a lot of flexibility: required parameters go into the initializer (new
), whereas optional arguments go into the method calls.
@Pauan nobody knows it yet, that's the problem. Please edit wiki to let people know they have to use "new" for Builder pattern.
There is no point in arguing anyway, you made the decision and thread has 0 movement since 2014.
Not sure what is more sad news today: Korean summit cancellation or this one.
@e-oz I haven't made any decision, I'm not a part of the Rust language team. I merely pointed out one (of multiple) ways to have required arguments with the builder pattern.
Also, that Wikipedia article is different from the builder pattern in Rust.
@Pauan patterns are not in any particular language, they are... universal. You can find that article has examples in multiple languages. And "done()" call is also your own invention.
By "you" I mean contributors.
The builder pattern doesn't really work when you are dealing with simple functions, unless you want to export a struct for each function.
I use PyCharm so I do get the nice tooling you mention but I don't have it when doing reviews on GitHub or elsewhere.
This argument
With optional arguments, I'd have to look at the function declaration.
is equally valid for the current state, you have no way of knowing what the parameters of system.open_valve(id, 1.00, false)
mean without looking at the declaration.
I can totally see the reasons for not wanting named args/kwargs in the language but that would be the biggest ergonomics win for Rust in my eyes.
Regarding 3, if the approach to implement follows Python, there shouldn't be any breakage no? Named arguments are opt-in at the call site in Python so you don't have to use them if you don't want to.
Regarding 3, if the approach to implement follows Python, there shouldn't be any breakage no? Named arguments are opt-in at the call site in Python so you don't have to use them if you don't want to.
The point is that if a function renames one of its arguments, that will break anything that is still calling it with the old argument name.
The point is that if a function renames one of its arguments, that will break anything that is still calling it with the old argument name.
when people rename builder's methods - it's not a disaster. But in this case it is. Absolutely not biased, lol.
@Pausan and then I take it that one must create a builder for the builder if those arguments are unclear as just plain numbers without a name? ;-)
The point is that if a function renames one of its arguments, that will break anything that is still calling it with the old argument name.
Just like any function? If you rename an argument, you will likely rename the builder fn as well in the case of a builder which would be a breaking change as well, unless you keep the old one around. I view the argument names being part of the type as a plus as an argument rename might mean a behaviour change that could go unnoticed otherwise
@Keats It's not about builders: if named arguments were added it would affect all functions.
For example, during the prototyping stage you might have a function with an argument called a
, and then you want to rename it to a better name like price
.
Or if you change the function's internal implementation then you would want to adjust the parameter names to match.
As @est31 said, people currently take advantage of that to rename function arguments in non-breaking ways, but they can't do that if named arguments were added.
You view them as being a part of the type because that's how it is in some other languages (like Python), but that's not how it is in Rust. So it would be a big change to the entire ecosystem.
There might be ways to avoid that particular problem: for example by requiring a #[keyword_arguments]
attribute, so that way if you don't use that attribute then you can rename the function arguments in a non-breaking way.
To be clear, I'm not against named arguments, I'm just clarifying facts so that the decision can be made with the most information.
I feel like some commenters are assuming that "named arguments" must mean all arguments to all functions have a name exposed that callers are allowed to use, and other commenters are assuming that exposing your names to callers is opt-in per-argument, and thus a lot of us are talking past each other.
Personally, I like the idea of named arguments, and I haven't thought about them all that much, but I do think they'd have to be something the function author opts in to in order for them to be a net win for Rust (or probably any language), especially since we didn't have them at 1.0.
@Pauan @Ixrec
Multiple variations of opt-in named args have been mentioned both in this thread and in a related internals discussion linked above. In fact, it looks like making them opt-in was the plan.
I would recommend that anyone thinking about contributing here read both threads. Otherwise we'll just keep retreading old arguments, which doesn't add much to the discussion.
@varkor yes the IDE argument is very flexible and applicable in multiple directions. You could e.g. argue that let
should be replaced by a 100 letter keyword and if someone has problems with that, they should change their IDE to make it appear as let
again... But here there is an existing IDE feature and I think it gives us two benefits:
a) you can configure it to display names whenever you want for your use case... I call this IDE induced foveation
b) crate authors can freely change argument names
optional arguments hide the fact that you can pass more arguments to the function.
This is also true of the builder pattern.
Though I do share your concern about forgetting to specify an optional argument. Perhaps rust should have a syntax like _
in match
to denote that you're opting into the remaining defaults.
I feel that named args/optional params/etc introduce a new way of doing things even while the existing way is well enough.
Rust already has two ways to approach this pattern Default
+ ..
and the builder pattern. I'd argue that neither is sufficient and present barriers to entry.
Default
is simple enough but has well known limitations (all or nothing).
The builder pattern feels odd and clunky if you come from a background of languages with named parameters. Even if you've seen it before the Rust builder pattern has nuances that don't exist in other languages which hurt its learnability: e.g. which flavor of self
should be used in the finalizer?, should it impl Default
, would a simple closure be better, etc.?
One way to avoid the chaos named arguments can bring is to only allow named arguments appear on private functions (that is, functions that are not marked as pub
and not a trait method). This way, the feature will still be pretty neat, and we can evolve on this path.
Another possible option would be to both introduce language syntax for
named arguments to distinguish them from positional ones (@name?) and
require that the names be used at call sites.
is to only allow named arguments appear on private functions
That doesn't solve my complaint with non-opt-in named arguments: that it changes improving a name from a _local_ question (since it can only affect the current function) to a _module_-level question. That's annoying for both humans and IDEs, and as a result discourages improving names.
- optional arguments hide the fact that you can pass more arguments to the function.
@est31 Thanks for listing out your concerns. This one is in a way crucial to optional args, since they're all about adding args without breaking existing code. But I agree that knowing whether an argument list is or is not exhaustive can be useful information when modifying code.
What would you think of a syntax foo(bar, ..)
to indicate that there are optional args being ommitted, and a rustfix-able clippy warning that hints to the foo(bar)
to foo(bar, ..)
conversion, so that you don't miss it when an optional argument is added?
optional arguments hide the fact that you can pass more arguments to the function.
Or they hide the fact that you can pass fewer arguments at the function call side.
But don't call any function if you don't know its signature and its intention.
I don't know why there is so much hate about optional args and named arguments, they are both syntactic sugar which is resolved by the compiler.
I don't know why this should be a breaking change since functions with default arguments are normal functions with the same arity as before but with the option to omit some arguments at the call side in favor to default settings suggested by the library/function creator. The omitted arguments are then filled up by the compiler.
Whereas named arguments don't change the arity of the function only the struct in which all named args are packed in. Named arguments are better than normal struct parameters in that they allow default values which don't have to be defined in a struct outside the function and which can be easily overdefined without to create a second default struct where only one value of 40 values has changed.
And no, keyword arguments are also type checked.
Optional and named arguments will significantly reduce the misuse of traits in order to achieve variability or optionality.
Variadic arguments are more powerful than arrays in that they can allow for heterogeneous types. Further variadics are some form of generics which allow to parametrize over function signatures.
@leodasvacas yeah foo(arg,..)
would fix my point if it were mandatory. Then it'd be the same tradeoff like for struct initializer syntax.
@leodasvacas adding to that: there is surely a tradeoff here of features. I don't think that there is a positive sum game result here, sadly :/.
Another point:
system.open_valve(id, 1.00, false, true)
system.open_valve(id, 1.00, false, true)
Hard to check:
system.open_valve(id, emit_event_when_open=false, send_drone=true, opening_speed=1.00)
system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true)
Even harder to check (you'd have to check the signature of the function to find out the default for emit_event_when_open):
system.open_valve(id, emit_event_when_open=false, send_drone=true, opening_speed=1.00)
system.open_valve(id, opening_speed=1.00 send_drone=true)
The last example can be fixed by a lint that checks whether you set a named optional arg to the default.
@est31 I'm sure rustfmt would be able to order your named arguments alphabetically.
Edit: Also we could require that arguments are used in the same order that they are declared, like Swift does. Python doesn't require this.
Permutating order means that checking two calls for equivalence turns from a O(n) problem to a O(n^2) problem.
If you put the args in a hashmap that's O(n). If you sort them its O(nlogn), for a small amount of arguments, or for the "visual implementation" of the sorting algorithm is sufficient, and is easily assisted by rustfmt as @leodasvacas notes.
If omission is allowed but not reordering, you could always just do an O(n)
walk over the original function definition to see which arguments are used at each callsite, I don't see why doing an O(n^2) blind comparison between two call sites is ever necessary:
def: system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true, other_val=1.00)
ex1: system.open_valve(id, __________________, emit_event_when_open=false, send_drone=true, other_val=1.00)
ex2: system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true, ______________)
Lol when I mentioned landau O I've originally meant how long it takes for me to compare the lists manually. But yeah if they are sorted such a comparison can be done in O(n)
. My brain doesn't have O(n) memory available though (at least I want to use it for different things, like reasoning about more interesting things than whether two function invocations are equal) nor do I want to compute hashes in my head so the unsorted case is still in O(n^2).
This is not really ever an issue in other languages that have keyword args though, e.g. Python.
long it takes for me to compare the lists manually.
I'd like to point out that this is also a problem in structs where there is no implied ordering of fields, and with the ..
syntax you may not even have all of the fields present. Builders also have similar issues, but there's less intent expressed to the compiler, so tooling like rustup
can't reorder the optional "params" in a way that's easy to compare. Worse yet with a builder the order of the calls may effect the semantics of the program!
foo(arg,..)
would get rid of the main purpose of the default arguments -- to make it so that you can extend an API by adding new arguments to a function (that previously had all mandatory arguments) without breaking existing callers. The common case for this is that you realize there is some additional aspect of the function's behavior that should be customizable, but where most of the time there is a sensible default (which is usually the original behavior prior to adding the new argument). If you didn't have the foresight to use the builder pattern or whatever special macro people are imagining, right now you're out of luck, and have to come up with a new function that takes the new argument or introduces the builder pattern.
Since we are brainstorming, I would like to pitch in my thoughts on this. The syntax inspiration is from Dart as well as Swift.
The arguments can be both positional and named & it is backwards compatible.
fn function(a: bool, { b: i32 });
=> function(false, b: 54);
fn function({ a: bool, b: i32 });
=> function(a: false, b: 54);
/// Not allowed.
fn function({ a: bool }, b: i32);
/// Current syntax still works
fn function(a: bool, b: i32);
=> function(false, 32);
/// Swift like external names for named arguments
fn start({ from start_loc: u32, to end_loc: u32 });
=> start(from: 4, to: 10);
With Default Arguments
fn function({ a: bool = false, b: i32 = 4 });
=> function();
=> function(a: true);
=> function(b: 6);
fn function(a: bool = false);
=> function();
=> function(true);
/// All allowed syntax
fn function({ a: bool, b: i32 = 4 });
fn function({ a: bool = false, b: i32 });
fn function(a: bool = false, b: i32 = 4);
fn function(a: bool, b: i32 = 4);
fn function(b: i32, a: bool = false);
fn function(a: bool, b: i32 = 4, { c: bool });
=> function(true, 5, c: false);
=> function(true, c: false);
fn function(a: bool, b: i32 = 4, { c: bool = true });
The order of the argument names:
Wow, I'm a total minority here; I'm actually against this because it involves going to the documentation more often because a function is more "magic" than before. When you stare at a piece of code and you can't tell what it's doing because the function name is very short, but the parameters don't tell you what is happening (a good reason for parameter names, but not optional params).
Optional params bloat such functions with leader code that has to deal with the optional elements, when really this "optional" stuff can be handled in alternate functions that do the preparation, and then call the canonical function.
In every language that has optionals that I've coded in, optionals have not made my code easier to maintain, optimise, or even read. In practice it adds bloat to every such function and reduces readability at the call site. It makes API changes harder because now you have many call sites that have different semantics based on arrity, rather than name. This is no good for search and replace let alone refactoring tools.
Nothing good will come with optionals, but param names and varags should not be conflated as the same beast, these do have purposes and it should be seen that it's OK to reject optionals, but implement param names / varargs.
I'm actually against this because it involves going to the documentation more often because a function is more "magic" than before. When you stare at a piece of code and you can't tell what it's doing because the function name is very short, but the parameters don't tell you what is happening (a good reason for parameter names, but not optional params).
Sure, it can be abused, like most language features, but that doesn't mean it doesn't have valid use cases.
Consider the constructor of RenderWindow
from SFML. (note that I'm a maintainer of rust-sfml, so I might be biased here)
sf::RenderWindow win(sf::VideoMode(640, 480), "My Application");
It is pretty clear that this creates a RenderWindow
with 640x480 resolution and the title "My Application". You don't even need to look at the documentation to figure out what it does. All the defaults for the optional args are very sensible. In fact, they literally call the default constructors of their respective types. The only exception is VideoMode
, which provides a very sensible default bit depth of 32.
Now let's consider the alternatives in Rust.
Do nothing fancy, just require all arguments to be explicit.
let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application", Style::default(), ContextSettings::default());
This definitely has the advantage of being explicit about everything, so there can be no misunderstandings. However, the extra explicit arguments given are all defaults. If they weren't explicitly provided, it would be very sensible to assume that the function uses the defaults.
What's the disadvantage? Well, simply put, it's annoying for the user to always have to explicitly provide defaults. If there are a lot of functions like this, it can wear the user down, and make them annoyed with the API. "Why do I always have to say (..., Foo::default(), Bar::default(), ...)? All I want is a window of this size and this title. This is annoying."
Although I do acknowledge, that a "serious" systems programming language might not want to prioritize convenience over explicitness. But Rust can be, and is used for developing applications, games, etc.. If we want to compete with all areas of C++, we might want to consider the convenience features that C++ provides.
let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application");
let win = RenderWindow::new_ext(VideoMode::new(640, 480, 32), "My Application", Style::Fullscreen, ContextSettings::new(...));
Now we have two functions that essentially do the same thing, only one provides sensible defaults, the other is explicit.
This has no advantage over defaulted params. RenderWindow::new
does the same "magic" as it would with optional params. Only that there are now 2 function names the user needs to remember. 4 if you wanted to make a different function for all combinations of optional args. Also the required arguments are all repeated in the type signatures for all the different functions. Not a very DRY approach.
This is the most commonly used "substitute" for optional args.
The usage would look like this:
let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").build();
let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").style(Style::Fullscreen).context_settings(ContextSettings::new(...)).build();
The advantage here is that it's explicit that this is a "builder", so it has optional args. No "surprise" optional args.
However, it has several disadvantages. The biggest one is API inflation. Now you have to have a RenderWindowBuilder
in addition to RenderWindow
, and builders everywhere where you wanted to have optional arguments. This leads to a very messy API if you have a lot of optional arguments. Note that I didn't even make a builder for VideoMode
. It would be very ugly to do that just for one optional argument.
It also makes the default case uglier. Now instead of just simply calling a function, the user has to create a builder, and call .build()
on it.
This would also look very ugly and unintuitive for functions that aren't constructors. Consider sf::RenderTarget::clear
.
The default usage is win.clear(); // clears with black color, sensible default
or win.clear(desired_color);
.
What would this look like with builders? win.clear_builder().color(desired_color).clear();
?
You can do fancy things with Rust traits. For example, you could implement Into<RenderWindow>
for different tuples. This is a poor man's version of optional args. It has all the "disadvantages" of optional args, and several more.
The API becomes harder to understand due to all the different generic types involved. The user can't look up the usage in one single place. They have to look for what implements Into<RenderWindow>
.
Compile times can potentially suffer, but probably not by much.
And finally, generic methods can't be called on trait objects, so this solution can't work in any context where dynamic dispatch is required.
Optional params bloat such functions with leader code that has to deal with the optional elements, when really this "optional" stuff can be handled in alternate functions that do the preparation, and then call the canonical function.
This isn't a problem if it's kept simple, like with SFML in the above examples.
In practice it adds bloat to every such function and reduces readability at the call site.
In the above examples, the alternate APIs had more "bloat" than an API with language-level optional args would. Again, readability is not a problem if the provided defaults are sensible.
It makes API changes harder because now you have many call sites that have different semantics based on arrity, rather than name. This is no good for search and replace let alone refactoring tools.
This is also true for many uses of macros and generics, which Rust supports in spite of this.
I would like to see default arguments have the same level of support as default copying of structs. After all, an argument list is isomorphic to a struct containing all the arguments. The one wrinkle is that you only want to give a Default
trait to part of the list, maybe not all of it. Reasoning this through, it suggests that
..
fn example(a: i32, { b: bool, s: String })
This doesn't suggest an obvious way to provide the defaults, however. The closest analog to an impl
section that is specific to a function is its where
clause, so perhaps it could go there. Alternatively, one could use { b: bool = true
-type syntax, but then this should be allowed for deriving struct defaults also.
It also doesn't address whether default arguments should be constants or whether they can be computed from the non-default arguments. Constants are less surprising, but computed defaults can be extremely useful in cases where you would other have to use ad-hoc sentinel values (e.g. if the default should be to match the length of an input).
I've proposed structural records (https://github.com/rust-lang/rfcs/pull/2584), it's not exactly named function arguments (so I'm keeping this issue open), but it overlaps a bit...
I've proposed _structural records_ (#2584), it's not exactly named function arguments (so I'm keeping this issue open), but it overlaps a bit...
Keep in mind that this issue is not just about named args, but also about optional args.
Keep in mind that this issue is not just about named args, but also about optional args.
And "variable-arity functions" which probably includes the huge topic of variadic generics. Considering this and the number of comments here, I wonder if this issue should be split into multiple ones.
For those interested in these features, check out https://github.com/rust-lang/rfcs/pull/1806#issuecomment-435680429!
Using #2584 in conjunction with #1806 could provide a solution that I believe would cover most of the use cases for named/default args.
Not sure if this point has already been raised, but I didn't see it:
For me, rust is all about eradicating classes of issues that plague other languages.
If we have named arguments, we could add a feature to the compiler to eradicate a very common programming bug:
fn doSomething(u8 x, u8 y) {...}
doSomething(y, x); //oh oh, the variables are in the wrong order!
If we have named arguments, we can make the compiler not allow positional arguments in the cases where you have 2 arguments with the same type, i.e the compiler will force you to write:
doSomething(x: x, y: y);
So this bug will never happen again.
@tzachshabtay even better, since in rust struct fields with matching names can and should drop the field: field
syntax, this:
doSomething(x: x, y: y);
becomes this:
doSomething(x, y);
while STILL KEEPING the compile time check that you used the named arguments correctly. WOW.
@berkus However this would mean that changing the name of the fields of a function call would be a breaking change.
Yes, for this the approach taken by Swift
lang could be better usable. This thing had been thought out there.
Seems many people agree that macros are appropriate for this task, yet I haven't seen a crate to generate such a macro.
So I wrote a proc_macro duang to implement it.
For example:
duang! ( fn add(a: i32 = 1, b: i32 = 2) -> i32 { a + b } );
fn main() {
assert_eq!(add!(b=3, a=4), 7);
assert_eq!(add!(6), 8);
assert_eq!(add(4,5), 9);
}
Personally, I would be strongly against optional/named arguments for a language such as Rust. It's a feature that belongs in the space of scripting languages such as python. You can simply use an existing datastructure:
fn optional_arg(optional: Option<i32>) -> i32 {
match optional {
Some(a) => a + 1,
None => 0
}
}
let answer = optional_arg(Some(1));
let answer_default = optional_arg(None);
dbg!(answer, answer_default);
(I should point out in fairness that my example uses dbg!
with optional arguments—conceded, that's what macros are for! It's about having a hygienic place for these sorts of language features.)
@crumblingstatue Functions with many unnamed arguments are discouraged, just like how tuples/structs with many unnamed fields are discouraged. Instead, you should use Rust's existing datastructure for named arguments: a struct. Then you could make default constructors that allow users to overwrite fields with Rust's struct update syntax.
. . . // After `RenderWindow`'s implementation
struct Window<'a> {
resolution: VideoMode,
title: &'a str,
style: Style,
settings: ContextSettings,
}
impl<'a> Window<'a> {
// Non-optional values in method signature
fn new(vm: VideoMode, s: &'a str) -> Self {
// optional values in returned struct
Window {
resolution: vm,
title: s,
style: Style::Fullscreen, // <-- default!
settings: ContextSettings::new(), // <-- default also!
}
}
}
fn main() {
let win = Window {
style: Style::Windowed, // Overwrite default to set an alternative preference
..Window::new(VideoMode::new(640, 480, 32), "My Application")
};
let win_rendered = RenderWindow::new(win);
. . . // more things with `win_rendered`
}
You can say that this is more verbose, and yeah, the way I've written it is. However—I consider it an advantage that the language lends itself to good API design; Creating the Window
struct allows end users to build a higher-level concept of what RenderWindow::new()
is supposed to do. As a bonus, it delineates a potentially helpful refactor; you can replace RenderWindow::new()
with the (less Java-y) Window::render()
. In essence, the sense that the function signature is too long is an indication that your API needs more building blocks.
I understand how folks coming from python naturally reach for the most 'convenient' syntax to express something, because that's the pythonic style. But there's a difference between syntax that is in line with a language's goals and what is more idiomatic to specifically you. Python is not averse to preferring some really arcane syntax; I feel comfortable writing my_list[:-1][::-1]
now that I've been using it for the past few weeks, but when I take breaks from my Python projects and come back to them, I have to look up all the shorthands again. While this suits python's aims of being expressive, it comes at the explicit tradeoff of language consistency. Rust, on the other hand, has very different goals than python. In light of Rust's stated goals of scaleability, maintainability, and building a culture of exceptional documentation, optional/keyword args are not appropriate for the language.
@ThomasKagan You say:
In light of Rust's stated goals of _scaleability_, _maintainability_, and building a culture of _exceptional documentation_, [...].
...but that is precisely the reasons why Rust _should_ have named and default arguments. With only positional arguments, your functions are _difficult to maintain_, because you will inevitably need to add a new parameter somewhere, which breaks all call sites. The alternative is, of course, to define a new function that also takes the new parameter, but now you have two functions that do almost the same thing. If you then need to add even more optional parameters, there's a combinatorial explosion in the number of functions you need. This does _not scale_ well (in terms of cognitive load) - it _dilutes the documentation_ because you have to explain the difference between all those almost-the-same functions, and anyone reading the calling code needs to spend more effort to keep track of several variations of the same function.
Parameter structs and the builder pattern do solve the same problems, but that is, again, precisely the point - it's a coping mechanism. It definitely has it places, but for many simple cases it's like using dynamite to dig a ditch just because there weren't any shovels around. The fact that we _can_ use design patterns to emulate missing language features is not always a good reason to accept those workarounds as desirable - on the contrary, it's a big reason why Java is so infamously verbose and clunky.
I should note that my main objection to verbose workarounds, like the builder pattern, is _not_ that you have to write a lot of code. It's that you have to _read_ a lot of code. Code that serves only to express implementation details behind the API. Boilerplate is not bad because you have to write it, it's bad because it's irrelevant noise that distracts from the relevant substance of the program.
I agree that functions with too many parameters is usually a code smell, but everything always depends on context, and too many parameters is mainly a problem when the arguments are all mandatory and not named. I think the benefits of named and default arguments far outweigh the risk that it might encourage someone to write functions that do too much.
@ThomasKagan
Then you could make default constructors that allow users to overwrite fields with Rust's struct update syntax.
This doesn't work with structs that have any private fields or which are marked #[non_exhaustive]
, so it's an incomplete solution at best.
@ThomasKagan
It's a feature that belongs in the space of scripting languages such as python
It works well in Swift and has been in use for ages in Objective-C.
Haha, I figured my view probably wouldn't be the most popular in this crowd. Tbh sometimes it's fun and healthy to be the devil's advocate. I just ask the folks who gave a thumbs down reaction to my post clarify what exactly they didn't like about it, so I can see their perspective. I'd say I side most with @crumblingstatue, once he referenced anonymous structs (#2584)
I don't consider user-facing, public structs 'bloat' if they appropriately model your api's data. I should mention that I would be considerably less averse to, particularly, named default args (because this thread seems to discuss a few different features all as one), for the maintainability reason @emlun mentioned—especially if you're required to use the keyword at call site. I see how it could be helpful in Rust's case for accomodating/updating legacy code. In the case of RenderWindow
, I would, personally, prefer my Window
suggestion, but in theory if you were truly very really sure that a use case needs just a shovel, then fine. But even too many named arguments is a code smell, and for that reason I was hesitant about adding too much nuance to my previous post.
IMO, reading more code isn't worse than reading less code that is less clear—like my python example, and don't get me started on javascript. It's about being able to find the parts that are useful to you right now, and understanding them.
@tikue I don't see how this makes proper data modelling for function args infeasible. But I am open to more comment.
@oblitum Could you delineate how those cases apply to rust as well? Why are those features in Swift and Objective-C, and how do those reasons align with Rust's goals?
@ThomasKagan
@oblitum Could you delineate how those cases apply to rust as well? Why are those features in Swift and Objective-C, and how do those reasons align with Rust's goals?
Read this comment and this.
It would be great to have this but my actual view is that the boat has sailed for Rust, this should have been in upfront, I think now it's late to have this.
In programming, it's really a sad mistake that the mathematical Euler function notation has become the standard, it's not self documenting, its place is math, not programming. Named parameters and return should have been the norm for programming.
Haha, yeah that's fair. I would like it if all arguments are required to be named, as opposed to the python interpretation of keyword arguments, where they indicate something is optional and the keyword itself is optional.
I personally see named args and optional args as two different issues. Named args, as previously mentioned, help eliminate entire classes of bugs involving positional args. As soon as you have multiple arguments with the same type, then the type system cannot help you any more.
I agree with the opinion that special Arg structs are one of the current best ways to solve this, but involve both boilerplate and code bloat, so I made a proc macro that generates Arg structs for methods, and then a macro that inserts the struct at the callsite:
#[named_args]
fn foo(a: i32, b: u32, c: String) {
println!("a: {}", a);
println!("b: {}", b);
println!("c: {}", c);
}
pub fn main() {
named!(foo(a: -4, b: 5, c: "n".to_string()));
}
Since this as implemented is just a syntax switch (what you type inside of the arg brackets gets transplanted into a foo_Args { ... }
struct expression) you keep all of the support that struct creation expressions have. You also cannot mix the two different forms inside a method call - you must use either named or positional syntax for all arguments. I suspect this also unlocks ..foo_Args::default()
syntax but I've not actually tried that.
Much like Future
vs await
, it seems to me that named args are a lot less controversial and can be added first. Furthermore, they IMO add real value just on their own and then we can have a different conversation about optional/variadic arguments.
E: looking at it again, I restricted the macro to just Ident
s, so the ..
syntax is not supported in my little test.
Additionally, I do _not_ see this as a subset of anonymous named structs, any more than positional function arguments are a subset of positional tuples (i.e., they're not). It would be ideal if the anonymous named structs had the same or similar syntax to named function arguments but they are different proposals with different goals.
In terms of syntax for named (not optional) arguments, let's look at the anonymous named struct RFC: https://github.com/Centril/rfcs/blob/rfc/structural-records/text/0000-structural-records.md
| Named version invocation | Positional version invocation
-- | -- | --
Struct | Yes, T { a: foo, b: bar)
| Yes, T(foo, bar)
Anonymous Type (value) | No, Structural Record RFC (possibly { a: foo, b: bar}
) | Yes, (foo, bar)
Function Call | No, this issue | Yes, f(foo, bar)
Open question 1: should there be a difference between functions with named arguments vs positional arguments? I.E. should named arguments be a semantic difference at the function declaration or just a syntactic one at the call site? Certainly there is a semantic difference between named structs vs positional structs, and probably between named anonymous types and positional anonymous types.
If not, then given that function declarations already have to name their arguments, one possibility is that named argument calls can just reuse those. Then, named arguments would be syntactic sugar over a call to the original function call with the compiler mapping your named arguments into the right position. The downside to this is we run into the issues with the public API, see the discussions about a potential fn foo(pub x: i32) {}
earlier.
Open question 2: how should this interact with arguments named _
?
Open question 3: how should this interact with the Fn
traits, since the names are lost? Should they just be unable to be called with named arguments?
EDIT: the answer to all 3 questions is "it should not", since as described in tanriol's comment below the named arguments cannot be inferred in general from all positional function definitions, and there instead does indeed need to be a separate opt-in syntax. One thing I don't like about the pub
syntax is that it's opt-in per field instead of opting in for the function.
...given that function declarations already have to name their arguments...
The usual reminder: functions do not have to name there arguments, they provide irrefutable patterns for them. Given a function
fn process_pair( (id, name): (u32, String) ) { unimplemented!() }
that can be written in Rust today, there's no single obvious way named arguments interact with it.
It seems to me then, that for such functions that don't name all their args, there's two possible approaches:
1) if we allow functions where some arguments are named and some are positional (I personally am against this) then only some of their arguments can be made callable by name, or
2) Those functions can't be named into name-callable functions and you'd get a compiler error if you tried. Something along the lines of:
The function foo has non-named arguments and can't be made into a named argument function.
#[named]
fn foo(_: i32)
^----- this argument does not have a name
Suggested fix: give the function argument a name like: fn foo(_a: i32)
By the way, this is an interesting question. The leading underscore at the moment is an internal marker for "unused" due to the unused lints skipping variables with leading underscore. However, being able to call the function by argument names freezes them for API stability, which means that adding or removing the leading underscore becomes a breaking change too.
The suggestion above highlights this in an unpleasant way – it suggests that you provide a parameter name explicitly telling the user this parameter is (at least initially) unused. This information is going to be both outdated and provocative ("why do I have to specify this if it's not used anyway?!").
Basically this would deprecate the convention of "underscore-prefix the names of unused parameters" with all the associated churn. The other technically possible way out would be to special-case and ignore leading underscore for the purposes of parameter naming, but that's something I would be strongly against.
...given that function declarations already have to name their arguments...
The usual reminder: functions do not have to _name_ there arguments, they provide irrefutable patterns for them. Given a function
fn process_pair( (id, name): (u32, String) ) { unimplemented!() }
that can be written in Rust today, there's no single obvious way named arguments interact with it.
Something like this?
fn process_pair(pair (id, name): (u32, String)) { unimplemented!() }
process_pair(pair: (42, "something"));
Which follows Swift, that employs spaces to separate argument labels in the formal parameter list. I don't see any conflict with irrefutable patterns.
I'll go into detail on each one:
keyword-based parameters (as opposed to position-based parameters)
It'd be a nice addition but it doesn't interact well with tuple destructuring. Actually, I think functions with many parameters is in most cases an anti-pattern and I favor creating types liberally to give your code context through the type system and write "small" functions (self-documenting code FTW).
optional parameters (where one provides a default value for them, usually in the parameter list)
Pass an Option<T>
, call .unwrap_or()
at the start of the function's body and shadow that variable. Not as sugary but a lot more explicit, which is something that Rust really focuses on. I'm against implementing any kind of syntatic sugar for this, it isn't worth it.
variable-arity functions (which can be seen as a generalization or variation on optional parameters, depending on how you look at it)
I hate this in C and how it's just a language-specific way to walk over a variable-sized list with no names. I very much prefer the Python way, and we can do that by passing any collection type with the requirements we need and knowing all the performance and feature trade-offs. Again, a lot more explicit and I'm specially against this one.
What I'd really like to see is functions with multiple signatures that have the same types but different destructuring patterns, so for those cases where you currently write a match
block that covers the whole function depending on some arguments, you don't use that block.
It'd be a nice addition but it doesn't interact well with tuple destructuring.
🤔, the previous comments were just asking why is that and provided examples demonstrating that doesn't look like a real issue.
Actually, I think functions with many parameters is in most cases an anti-pattern and I favor creating types liberally to give your code context through the type system and write "small" functions (self-documenting code FTW).
https://github.com/rust-lang/rfcs/issues/323#issuecomment-508955630
FWIW, Gleam, a statically typed language for the Erlang VM, written in Rust, follows Swift at providing labeled arguments:
🤔, the previous comments were just asking why is that and provided examples demonstrating that doesn't look like a real issue.
I read that. For consistency that would require to bring the whole argument label/parameter name thing, which I'm not sure I like. But if it's backwards-compatible _and optional_, I'm not totally opposed to it, just not interested enough.
FWIW, Gleam, a statically typed language for the Erlang VM, written in Rust, follows Swift at providing labeled arguments
So...?
So...?
No argument, hence FWIW.
@ThomasKagan
Personally, I would be strongly against optional/named arguments for a language such as Rust. It's a feature that belongs in the space of scripting languages such as python
There are default arguments in C++, Kotlin has named arguments and default arguments. I don't see anything wrong with this, that some useful things come to us from scripting languages. Probably, the default arguments aren't so necessary, but having named arguments would be a great feature of Rust. Position-independent arguments improve the durability of the public and internal API and will not allow it to be broken due to ridiculous arguments movements or adding arguments.
Probably, the default arguments aren't so necessary, but having named arguments would be a great feature of Rust. Position-independent arguments improve the durability of the public and internal API and will not allow it to be broken due to ridiculous arguments movements or adding arguments.
The beautiful thing about the combination of named and optional arguments is that designing API's with a lot of parameters gets much easier.
Especially for cases where you have 3+ boolean parameters that are all optional it's just very neat to work with positional parameters.
fn search(query: String, include_images: bool, include_news: bool, use_cache: bool)
search("foo", false, true, false)
search("foo", false, false, true)
// vs.
// Using the star to signal that the rest are keyword arguments
fn search(query: String, *, include_images: bool=false, include_news: bool=false, use_cache: bool=false)
search("foo", include_news=true)
search("foo", use_cache=true)
Not only is the latter more readable, it's way less error prone. This is a very simple proposal and resembles Python, but the *
could obviously be replaced by pretty much anything (e.g. a pub
or a ~
, ^
, @
or whatever).
I would probably advice against mixing positional/keyword arguments (unlike Python, where almost all arguments are potential keyword arguments). This makes it so parameter names in older Rust code won't suddenly become part of the API.
So basically we're just talking about some potential special sugar for
do_thing(ThingArgs{ use_cache: true, include_news: false});
Right?
Yes, I guess pretty much everything is possible already if you combine functions and structs.
Another way to write my proposal would be to work with something like "anonymous" structs, which could look like this: search("foo", {use_cache: true, include_news: false});
. IMO this is just uglier, but it would probably serve the same purpose.
For the compiler the easiest way to understand this would probably to create "anonymous" structs.
@Lokathor
The difference is that we don’t need to pack arguments into a structure.
Yes, I guess pretty much everything is possible already if you combine functions and structs.
I tried wrapping my head around the idea of naming things that implement a trait without success:
fn foo(i: i32, get_int: impl Fn() -> i32, consume_int: impl Fn(i32));
foo(3, || 45, |x| println!("{:?}", x));
Versus something like:
fn foo(i: i32, pub get_int: impl Fn() -> i32, pub consume_int: impl Fn(i32));
foo(3, get_int: || 45, consume_int: |x| println!("{:?}", x));
Since struct can't have field that impl
trait, it's not possible to use the builder pattern with no overhead (you will have dynamic dispatch with dyn
instead of static dispatch).
Trait aliases aren't a solution either.
@Lokathor - Function arguments' lifetimes aren't treated the same way as struct field lifetimes, which is another way that it's not just sugar. (Or it's a lot more sugar than what you showed, including struct creation, automatic handling of lifetimes, and destructuring to get at all the pieces.)
@robinmoussu It is possible using a struct, it simply needs to be a generic struct (Playground):
struct FooOptions<F, G> {
get_int: F,
consume_int: G,
}
fn foo<F, G>(
i: i32,
FooOptions {
get_int,
consume_int,
}: FooOptions<F, G>,
) where
F: Fn() -> i32,
G: Fn(i32),
{
// ...
}
For information, I finally managed to have named argument with default value for impl
trait in rust stable.
Wall of code (click to expand)
This works in stable
// It's needed to create a separate type with a similar name than get_int
pub struct GetInt<F>(F)
where F: Fn() -> i32;
// Cannot use Fn() -> i32, must be a concrete type
impl Default for GetInt<fn() -> i32>
{
fn default() -> Self{ GetInt(|| 1) }
}
pub struct ConsumeInt<F>(F)
where F: Fn(i32);
// Cannot use Fn() -> i32, must be a concrete type
impl Default for ConsumeInt<fn(i32)>
{
fn default() -> Self{ ConsumeInt(|x| println!("{:?}", x)) }
}
pub fn foo
(
i: i32,
get_int: GetInt<impl Fn() -> i32>, // Repetition of the impl trait
consume_int: ConsumeInt<impl Fn(i32)>
)
{
for _ in 0..i {
consume_int.0(get_int.0()); // Horrible .0 syntax
}
}
fn main() {
foo(
3,
GetInt(|| 45),
ConsumeInt(|x| println!("{:#}", x)),
);
foo(
2,
GetInt::default(),
ConsumeInt::default(),
);
}
This would be much better
pub fn foo
(
i: i32,
pub get_int: impl Fn() -> i32 = || 1,
pub consume_int: impl Fn(i32) = |x| println!("{:?}", x)
)
{
for _ in 0..i {
consume_int(get_int()); // note: no .0
}
}
fn main() {
foo(
3,
get_int: || 45,
consume_int: |x| println!("{:#}", x),
);
foo(
2,
get_int::default(), // note: placeholder syntax
consume_int::default(),
);
}
For each named, defaultable argument, this adds:
It works, but to be honest it really reminds me the bad part of C++ template meta-programming.
For each named, defaultable argument, this adds:>
6 lines of boilerplate
an additional public type.
Hmmm, perhaps this can be turned into something practical by some sort of magic macro?
@norru This is not a realistic option, and has been discussed over and oven in this thread. @david-mcgillicuddy-moixa did a POC as well as @xiaoniu-578fa6bff964d005 here. Proc macro are bandage that are ok-ish for your own application crate. But as soon as you want to import them from an external crate, it's not really convenient to use. I don't expect either that I could use those macro if I try to contribute to a crate I'm not the maintainer.
This discussion starts to be long, I will try to sum-up what I found the most interesting:
It's worth noting that named arguments, default arguments and variable-arity functions can all be implemented separately. I will only talk about named arguments and default arguments since I think they are the most important feature to have.
I think that @crumblingstatue's comment explains the best why having default argument is important.
Probably what was the best proposition so far for named argument was done by @azerupi and has done a pre-RFC. It has a parsing ambiguity (as explain in the summary with type ascription, but it's not too late since type ascription is not in stable yet (it's only feature gated), and not widely used. I personally think it would be better to change the syntax of type ascription to allow named arguments.
To give you the gist:
// 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;
This can be easily extended to support default value for arguments:
// Function with default named arguments
pub fn default_arguments(pub a: i32 = 0, pub b: i32 = 2) -> i32;
More details could be found in the mentioned pre-RFC.
And to add my own opinion, I think that (like in C++), default argument should just be syntactic sugar at the call site (ie. copying the expression of the "initializer" at the call site) witch simplify a lot of the issue.
pub fn(a: int, foo: SomeType = bar("baz")) { ... }
// when writting
foo(42);
// it will be expanded at compile time to
foo(42, bar("baz"));
At call site, to allow future addition of new arguments with default value, no additional syntax should be needed, but a trailing , ..
could be added to be explicit and warned by default with clippy:
// this compiles but clippy warns about it
foo(42);
// this compiles and emit no warning
foo(42, ..);
Note: ..
is syntax for a full range
Probably what was the best proposition so far for named argument was done by @azerupi and has done a pre-RFC. It has a parsing ambiguity (as explain in the summary with type ascription, but it's not too late since type ascription is not in stable yet (it's only feature gated), and not widely used. I personally think it would be better to change the syntax of type ascription to allow named arguments.
I think type ascription (which we already have, e.g. in let pat : type = expr;
or fn foo(pat : type)
) is substantially more important than named arguments and I think that type ascription should uniformly use the obvious term : type
syntax as in most similar languages. So I do not agree with having type ascription on expressions change its syntax, especially when https://github.com/rust-lang/rfcs/pull/2584 (structural records) give you named arguments in a principled way, which works well with the type system overall, and is generally more useful. So all in all, I think I'm not in favor of named arguments as something other than structural records (or possibly anonymous types, but they are less useful).
For those who wants the context but don't want to open external links, #2584:
Introduce structural records of the form { foo: 1u8, bar: true } of type { foo: u8, bar: bool } into the language. Another way to understand these sorts of objects is to think of them as "tuples with named fields", "unnamed structs", or "anonymous structs".
Specifically about named arguments:
Structural records could be considered to lessen the need for named function arguments by writing in the following style:
fn foo(bar: { baz: u8, quux: u8 }) -> u8 { bar.baz + bar.quux }
fn main() { assert_eq!(3, foo({ baz: 1, quux: 2 })); }
With those limitations:
- With structural records, you cannot construct them positionally.
- You cannot adapt existing standard library functions to use named arguments.
@Centril First of all, I would like to apologize, for some reason when I read this whole thread I didn't really like (initially) your proposition, and kind of missed it.
I think it solve the issue of named argument, and is totally compatible with a (possible) future addition of default value (like your proposition on default value for struct).
I also don't think that the limitations you mentioned are an issue, because it can be easily solve in the future. If rust ever get something like star pattern in python (ie. expanding the content of a struct in-place) both of those issue disappears. If expanding was done through a magic macro expand!
witch sort the arguments as the enclosing context expect, we could write:
fn foo(bar: u8, baz: u8);
let foo_args = {baz: 2, bar: 1}; // Same name, but not the same order the the args of foo()
foo(expand!(foo_args)); // expanded as foo(1, 2)
All in all, is there somewhere an overview/ETA of the global progress of all those features? I tried to do it in this thread, but it was quite hard to find the current state of all discussions and implementation.
All in all, is there somewhere an overview/ETA of the global progress of all those features? I tried to do it in this thread, but it was quite hard to find the current state of all discussions and implementation.
Not beyond what is noted in my draft (which I will get to finishing eventually) and the structural records RFC. As you may imagine, the language and compiler teams are swamped with work already, cleaning up years of technical debt.
Structural records might be useful in some cases, but I think REAL keyword args (together with REAL optional / default args and variadic args) looks more clear and is easier to use.
I don't particularly agree with this perspective. I find foo(param: expr)
to be confusing and to fit poorly with the general design of the language due to the trait system, patterns, etc. Moreover, "keyword args" as construed here is also limiting and far less powerful than structural records. In particular, while structural records permit building up arguments first into a let
binding, grouping things into different records, more generally treating the inputs as a data type, keyword args do not.
On the other hand, using structural records to emulate keyword arguments makes it a tradeoff. You have to decide, "this function has a ton of arguments, so it's worth making it accept a record instead, even at the cost of making all callers use the ugly func({args})
syntax and requiring all arguments to be labeled." You can't just say, "hey, this bool
argument is a little unclear, so I'll just tack on a label". (Or if you don't like the bool example because you can use enums instead, how about: "hey, in this copy-file function the user might forget which argument is the source and which is the destination, so I'll label them".) That's why I dislike the structural-record approach. If you're prepared to require extra ceremony, named records work pretty well as it is.
I think that's probably a good thing though. You should really be avoiding designing function calls so complex that you'd set up a full struct as the args in the first place. Not that there's never a place for such a thing, but that it's something to really really avoid.
That's some to really really avoid, but I'd like to point out that this line of reasoning should simply be put fully aside when actually designing the feature or a language, the design shouldn't be driven from worst cases, otherwise the language would simply provide no feature at all, as any feature characteristics can be abused. That's the job for language best practices.
I do not agree whatsoever that my_fun({ foo, bar, baz });
is ugly. The syntactic overhead is minimal. Moreover, and unlike named arguments, you can build up each bit in let
statements and then use the field shorthand mechanism to avoid having to write foo(name: value, ...)
.
I tend to agree with @Lokathor that a function with so many parameters is a poorly designed one, and there are limits to how convenient we should make such poor designs. I do also think that requiring names at the definition site is fine. When there is such a bool
parameter (not that I would find that good design), past experience tells me that labeling just one call site would be insufficient.
I tend to agree with @Lokathor that a function with so many parameters is a poorly designed one, and there are limits to how convenient we should make such poor designs.
True keyword arguments can help functions with any number of parameters, such as foo(true)
or copy(source, dest)
. You could put them on all your functions if you wanted. It's only the structural-record workaround that, by giving "keyword arguments" syntactic overhead, effectively assumes they should be reserved for extraordinary cases, like functions with large numbers of parameters.
When there is such a
bool
parameter (not that I would find that good design), past experience tells me that labeling just one call site would be insufficient.
Well, the space of possible designs includes ones where the keyword would be mandatory at call sites, similar to Swift (and to Rust structs). That admittedly would have the significant downside that keywords couldn't be retrofitted onto existing functions, but it avoids the "just one call site" issue. (Note that requiring keywords doesn't contradict my earlier objection to "requiring all arguments to be labeled", since you could mark only a subset of a function's arguments as keyword arguments.)
Even if keywords are optional at call sites, my past experience (from Python) tells me that labeling bool arguments can work just fine. It's not hard to remember to label them at all call sites.
It's only the structural-record workaround that, by giving "keyword arguments" syntactic overhead, effectively assumes they should be reserved for extraordinary cases, like functions with large numbers of parameters.
On the other hand, field shorthands combined with structural records reduce overall syntactic overhead as compared to keyword arguments at call sites. I also don't agree that the minor overhead involved would result in usage restricted to functions with large number of parameters. I would personally use structural records for semantic grouping, whether there be a small or large number of parameters (but preferably the former).
Even if keywords are optional at call sites, _my_ past experience (from Python) tells me that labeling bool arguments can work just fine. It's not hard to remember to label them at all call sites.
Labeling bool
arguments only tells you what the meaning of that argument is within the function (the "what"). It does not however tell you the "why" of passing true
in this specific location, which is, in my experience, far more important.
Labeling
bool
arguments only tells you what the meaning of that argument is within the function (the "what"). It does not however tell you the "why" of passingtrue
in this specific location, which is, in my experience, far more important.
I don't understand your objection here. How is copy(src, dest, allow_overwrite=true)
less clear than copy(src, dest, OverwriteMode::Allow)
? They both tell you the "what" and neither tells you the "why". Like @comex, I've used and encountered this pattern a lot in Python, and I've always found it convenient for both reading and writing.
@johnw42 without getting into all my thoughts on this thread, I can state a key difference in your example. The former (keyword arguments) assumes only a single option for allow_overwrite
whereas the latter describes an OverwriteMode
which I can imagine at least three variants for: Allow
, Append
, Disallow
. Neither is intrinsically better, but it's clear to me how to encode the more general case into arguments with structs or enums, whereas I wouldn't want to always encode my function's arguments into keywords.
I think a stronger argument for keyword arguments is that the copy
function probably takes two Path
arguments (fn copy(src: Path, dest: Path, ...) -> Result<...>
), which could be mistakenly given in the wrong order, i.e. copy(my_dest, my_src, ...)
. This could typecheck, but do very much the wrong thing.
@johnw42 OverwriteMode.:Allow
is typed differently from other bool
s so the probability of messing up is even less. Moreover, OverwriteMode
conventiently allows you to add doc comments and this can be passed to several functions, thereby connecting them (sorta the same with structural records by the way as they are a separate type). As for neither telling you the "why", that's true, but also substantially diminishes the usefulness of keyword arguments. At any rate, there are other reasons (e.g. that keyword arguments composes poorly with a ML/Haskell-like language like Rust) which makes me opposed to keyword arguments irrespective of whether we have structural records or not (those records being useful for other things).
(e.g. that keyword arguments composes poorly with a ML/Haskell-like language like Rust)
How? And does that apply to labelled/named parameters/arguments? Gleam, which is more ML/Haskell-like than Rust, and that has been mentioned previously, follows Swift at providing labelled parameters/arguments.
Unlike Gleam, Rust (and Haskell), do have type classes. In Rust, this is specifically used for the Fn
trait hierarchy. This makes for composability problems:
fn foo(hof: impl Fn(u8)) { // <-- how do you call the HoF with keyword args?
hof();
}
// Ostensibly you could do the following, but that adds more special cases:
fn foo(hof: impl Fn(pub name: u8)) {
hof(name: 42);
}
And I think you also need to think about user defined traits:
trait Foo { fn bar(&self, pub name: u8); }
What are the rules here wrt. generic functions taking <T: Foo>
or using concrete types instead? Is the named parameter attached to the trait or the implementation, or both? I think rules can be devised here that makes sense, but I think they would be ad-hoc and substantially more complicated than structural records, which do not interact in a special way with traits (other than re. coherence, but that's common to structural things).
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.
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.
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.
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?
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.
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.
Fluent interfaces are nice, but:
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 :-)
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.
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.
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.
Fair enough, I'm convinced now this (what you said) is better.
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.
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));
I can't really do much, but I think this feature is necessary.
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
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.
Thanks!
If someone compiled the list of proposed solutions and then we all had a vote on it, would that help move this issue forward?
There is a RFC for named arguments: https://github.com/rust-lang/rfcs/pull/2964
+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)
}
First let me say that I am surprised after 6 years this issue is still unresolved.
Second, there is already a process for Option
al 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 Option
s:
// 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.
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.
Most helpful comment
Sure, it can be abused, like most language features, but that doesn't mean it doesn't have valid use cases.
Consider the constructor of
RenderWindow
from SFML. (note that I'm a maintainer of rust-sfml, so I might be biased here)sf::RenderWindow win(sf::VideoMode(640, 480), "My Application");
It is pretty clear that this creates a
RenderWindow
with 640x480 resolution and the title "My Application". You don't even need to look at the documentation to figure out what it does. All the defaults for the optional args are very sensible. In fact, they literally call the default constructors of their respective types. The only exception isVideoMode
, which provides a very sensible default bit depth of 32.Now let's consider the alternatives in Rust.
1. Explicit arguments
Do nothing fancy, just require all arguments to be explicit.
let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application", Style::default(), ContextSettings::default());
This definitely has the advantage of being explicit about everything, so there can be no misunderstandings. However, the extra explicit arguments given are all defaults. If they weren't explicitly provided, it would be very sensible to assume that the function uses the defaults.
What's the disadvantage? Well, simply put, it's annoying for the user to always have to explicitly provide defaults. If there are a lot of functions like this, it can wear the user down, and make them annoyed with the API. "Why do I always have to say (..., Foo::default(), Bar::default(), ...)? All I want is a window of this size and this title. This is annoying."
Although I do acknowledge, that a "serious" systems programming language might not want to prioritize convenience over explicitness. But Rust can be, and is used for developing applications, games, etc.. If we want to compete with all areas of C++, we might want to consider the convenience features that C++ provides.
2. Different functions for implicit/explicit args
let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application");
let win = RenderWindow::new_ext(VideoMode::new(640, 480, 32), "My Application", Style::Fullscreen, ContextSettings::new(...));
Now we have two functions that essentially do the same thing, only one provides sensible defaults, the other is explicit.
This has no advantage over defaulted params.
RenderWindow::new
does the same "magic" as it would with optional params. Only that there are now 2 function names the user needs to remember. 4 if you wanted to make a different function for all combinations of optional args. Also the required arguments are all repeated in the type signatures for all the different functions. Not a very DRY approach.3. Builder pattern
This is the most commonly used "substitute" for optional args.
The usage would look like this:
let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").build();
let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").style(Style::Fullscreen).context_settings(ContextSettings::new(...)).build();
The advantage here is that it's explicit that this is a "builder", so it has optional args. No "surprise" optional args.
However, it has several disadvantages. The biggest one is API inflation. Now you have to have a
RenderWindowBuilder
in addition toRenderWindow
, and builders everywhere where you wanted to have optional arguments. This leads to a very messy API if you have a lot of optional arguments. Note that I didn't even make a builder forVideoMode
. It would be very ugly to do that just for one optional argument.It also makes the default case uglier. Now instead of just simply calling a function, the user has to create a builder, and call
.build()
on it.This would also look very ugly and unintuitive for functions that aren't constructors. Consider
sf::RenderTarget::clear
.The default usage is
win.clear(); // clears with black color, sensible default
orwin.clear(desired_color);
.What would this look like with builders?
win.clear_builder().color(desired_color).clear();
?4. Fancy magic with Rust generics
You can do fancy things with Rust traits. For example, you could implement
Into<RenderWindow>
for different tuples. This is a poor man's version of optional args. It has all the "disadvantages" of optional args, and several more.The API becomes harder to understand due to all the different generic types involved. The user can't look up the usage in one single place. They have to look for what implements
Into<RenderWindow>
.Compile times can potentially suffer, but probably not by much.
And finally, generic methods can't be called on trait objects, so this solution can't work in any context where dynamic dispatch is required.
Addressing the rest of the arguments
This isn't a problem if it's kept simple, like with SFML in the above examples.
In the above examples, the alternate APIs had more "bloat" than an API with language-level optional args would. Again, readability is not a problem if the provided defaults are sensible.
This is also true for many uses of macros and generics, which Rust supports in spite of this.