Open pnkfelix opened 10 years ago
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
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.)