Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default and positional arguments #257

Closed
wants to merge 1 commit into from

Conversation

KokaKiwi
Copy link

Add defaults and positional arguments for function/method/closure declaration/call in Rust.
This RFC proposal is written from the ideas and suggestions brought in the corresponding feature issue in Rust main repository rust-lang/rust#6973

Rendered view

@blaenk
Copy link
Contributor

blaenk commented Sep 23, 2014

I'd love to at least have keyword arguments. Self-documenting calls are pretty nice. I think Swift does it well, but probably overly-complex for Rust. Default arguments are also convenient, especially if it's an issue with bindings like you say.

The sugar also elegantly maps to default arguments I think, as long as it's still possible to pass an explicit Option. Perhaps arguments can be made about it being confusing, since the type is Option but it's taking on a raw value. I don't think it's confusing though, because ultimately the Option is used within the function to signify whether or not the parameter was passed. In the worst case though if the sugar isn't included, it's not too difficult to just wrap it in Some, but it would be nice sugar to have I think.

@LeoTestard
Copy link
Contributor

I think it would be a good thing too to have both in Rust, though I wait for the comments of the core team to form a precise opinion about it.

The issue about the traits is interesting too. While in most cases, you expect indeed all the implementors to implement the same semantics and thus to use the same default value for an argument (which should then be specified by the trait, and described in its documentation), I think in some cases, the public interface of a trait might just expose the fact that an argument is optionnal, and leave the actual value of this argument as an implementation detail.

That being said, I haven't found an actual use case for this, I just have the intuition that both approaches could be useful, but I may be wrong,

@Valloric
Copy link

This is a lovely RFC that provides a much more usable language API over #258.

@theypsilon
Copy link

I think this would be nice in the language, but I don't like this part:

fn toto(a: uint, b: uint = 2, c: uint) { ... }
toto(1, c: 22, 3); // Resolved as toto(1, 3, 22)
                   // Same as std::fmt format string definition.

I think it is quite confusing. Maybe would be better to set a simpler rule, like: if you alter the declaration parameter ordering in the middle of a function invocation, you are forced to specify the parameters name for the rest of the function invocation.

So you would be forced to do this instead:

toto(1, c: 22, b: 3);

@arthurprs
Copy link

Why not use "=" to define positional arguments (instead of ":"). This keeps things consistent with the default declaration and other languages, like Python.

@netvl
Copy link

netvl commented Sep 24, 2014

@arthurprs, I believe this is done to be consistent with structure instantiation:

SomeStruct {
    field1: value1,
    field2: value2
}

And it does look good, I think.

+1 for the RFC, it would make writing expressive APIs much easier, and a possibility for adequate C++ bindings are also a huge win.

@reem
Copy link

reem commented Sep 24, 2014

How would this interact with FFI (exporting Rust functions to C)? Also, does the default value for an argument need to be Copy? Can Clone be used to enable default arguments for owned types? These are important questions this RFC does not answer.

@bvssvni
Copy link

bvssvni commented Sep 24, 2014

+1 Would be nice to have non-static expressions. Agree with @theypsilon about not allowing unnamed parameters after the first named one when you alter declaration parameter ordering.

@Veedrac
Copy link

Veedrac commented Sep 24, 2014

@bvssvni That's not quite what was said:

if you alter the declaration parameter ordering in the middle of a function invocation, you are forced to specify the parameters name for the rest of the function invocation

This is useful if you have a function like:

function(variable, 0, variable, variable, variable)

and you want to write:

function(variable, name: 0, variable, variable, variable)

It's the reordering that's problematic.

@arcto
Copy link

arcto commented Sep 24, 2014

I posted a proposal, on Rust Discuss, for something similar to this but instead by using syntactic sugar for structs.

The function would have to take a struct by copy or by reference.

@mangecoeur
Copy link

+1 for RFC. Personally used to using this in Python, its one of the most useful things ever for designing APIs that don't need a dozen similarly named functions for slight variations of options (or that have to take a very un-informative and verbose "options" object)

@jsanders
Copy link

@arthurprs, @netvl: Using : for struct initialization has been criticized in the past, because it would be more consistent to think of : as a type-related operator, while = is clearly a value-related operator. Of course changing struct initialization syntax isn't very realistic at this point, but I think adding more inconsistency in a brand new feature would be a mistake. Using the same = syntax at declaration and call would be much better.


* API design. Some functions in the Rust standard distribution libraries or in third party libraries have many similar methods that only vary by the presence of an additional parameter. Having to find and to memorize different names for them can be cumbersome.

* If this feature can be added to Rust, it can't be added post-1.0, as a lot of functions in the Rust standard library must be rewritten in order to benefit this feature.
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit strong, as only stable libraries would have issues with changing, and not all of the standard library will be stable. I'm also not sure that they can't be changed in a backwards compatible way.

Choose a reason for hiding this comment

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

They could be changed in a source backward compatible way by adding default parameters to existing methods as long as the then useless methods were kept.

Copy link
Author

Choose a reason for hiding this comment

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

@steveklabnik

and not all of the standard library will be stable

Rust standard library API isn't supposed to be frozen post-1.0?
If so, that's what I mean when I say it can't be added post-1.0, as the API itself should be changed in order to use default args.
And add it post-1.0 could lead to keep "old-fashioned" functions in the standard library (I think)

@xgalaxy
Copy link

xgalaxy commented Sep 24, 2014

if you alter the declaration parameter ordering in the middle of a function invocation, you are forced to specify the parameters name for the rest of the function invocation.

I agree with this! It would be really confusing if this wasn't the case. This is a much better rule than the one proposed in the RFC.


* If this feature can be added to Rust, it can't be added post-1.0, as a lot of functions in the Rust standard library must be rewritten in order to benefit this feature.

* Foreign bindings. This has been discussed on the mailing list recently about Qt5 bindings. Binding functions that make heavy use of default arguments or operator overloading can be very messy with the current system.

Choose a reason for hiding this comment

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

I do not see how this helps.

If I remember correctly, the main issue raised was the absence of overloading in the presence of a variety of parameters. fillRectangle has 12 overloads, for example (it should have 16 to be consistent), and even trimming it down to the bare essential it retains 2:

fillRect(QRect const&, QBrush const&)
fillRect(QRectF const&, QBrush const&)

Whether you have default or positional arguments does not help in generating bindings here.

And in general, you can generate bindings by simply "forgetting" default arguments. Certainly the resulting interface is less friendly (you now have to specify all parameters on each call), but it is automatically generated with no pain.

Thus, you might actually be undermining what is otherwise an interesting proposal. I would simply consider dropping that motivation.

Copy link
Author

Choose a reason for hiding this comment

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

As overloading is not the purpose of this RFC, I think this motivation is "correct" as it help fixing some issues in FFI design with C++ code (not all, but at least the "default arguments" one)

@liigo
Copy link
Contributor

liigo commented Sep 24, 2014

+1
2014年9月25日 上午12:48于 "James Sanders" [email protected]写道:

@arthurprs https://github.com/arthurprs, @netvl
https://github.com/netvl: Using : for struct initialization has been
criticized in the past, because it would be more consistent to think of :
as a type-related operator, while = is clearly a value-related operator.
Of course changing struct initialization syntax isn't very realistic at
this point, but I think adding more inconsistency in a brand new feature
would be a mistake. Using the same = syntax at declaration and call would
be much better.


Reply to this email directly or view it on GitHub
#257 (comment).

@summerlight
Copy link

I think visibility of a parameter name should also be considered. It is quite important since there might be someone who does not want to break backward compatibility of his library by simply changing a parameter name, and it cannot be easily addressed post-1.0.

@ftxqxd
Copy link
Contributor

ftxqxd commented Sep 25, 2014

Using = would be ambiguous, as foo = bar is a valid expression (returning ()). Using : as the RFC suggests would be unambiguous today, but become ambiguous in the future if we ever get type ascription (the ability to say let foo = bar(): int;, which uses a colon to specify the type of an expression), which is something that has been wanted for a long time. An alternative that is unambiguous even with type ascription would be =>.

@pnkfelix
Copy link
Member

@summerlight avoiding unintentional dependence on parameter names is a good goal. (Though I'll note that it would be an issue even if the core team does not get around to addressing this until post 1.0)

Maybe the RFC could be amended to use pub as a marker on formal parameters whose names can be used as keywords in the argument list.

@KokaKiwi
Copy link
Author

@theypsilon: I think it is quite confusing. Maybe would be better to set a simpler rule, like: if you alter the declaration parameter ordering in the middle of a function invocation, you are forced to specify the parameters name for the rest of the function invocation.

@bvssvni: Agree with @theypsilon about when you alter declaration parameter ordering.

@xgalaxy: I agree with this! It would be really confusing if this wasn't the case. This is a much better rule than the one proposed in the RFC.

Actually, the rule I wrote in this RFC is the same as parameter ordering in format string from std::fmt module.
But maybe it's not very effective for default/named args, that's why I talked about C++ (and Python) way to handle this.
However, I don't really know what's the best solution for this case. :(

@reem: How would this interact with FFI (exporting Rust functions to C)? Also, does the default value for an argument need to be Copy? Can Clone be used to enable default arguments for owned types? These are important questions this RFC does not answer.

As the default/named args is done by "just" sugaring function calling, the default values doesn't have constraints like this, as they are "just" placed automatically at call-time.
But if we allow non-static default values, maybe it could be useful.

@summerlight: I think visibility of a parameter name should also be considered. It is quite important since there might be someone who does not want to break backward compatibility of his library by simply changing a parameter name, and it cannot be easily addressed post-1.0.

@pnkfelix: Maybe the RFC could be amended to use pub as a marker on formal parameters whose names can be used as keywords in the argument list.

I'm not sure about what's the issue with parameter names.
If the caller use named args to call a function and the function's author change args names, I think it's "normal" if the compiler throw an error at call-time if the user update the function's library, as it's the same case if you change, for example, the name of a struct member used in a library.

@blaenk
Copy link
Contributor

blaenk commented Sep 25, 2014

I think visibility of a parameter name should also be considered. It is quite important since there might be someone who does not want to break backward compatibility of his library by simply changing a parameter name, and it cannot be easily addressed post-1.0.

I think this touches on how Swift does things, where it separates parameter names into local (for use within the function body) and external (for use at the call-site) parameter names (inherited from Objective-C where both are mandatory, though not in Swift). This separation allows the local parameter name to be changed without breaking the external interface/dependent clients.

I think this is a very practical and elegant way of reconciling flexibility with Swift's intended goal of first-class interop with Objective-C, where both local and external parameter names are required in every method declaration (IIRC).

By default, only local parameter names are given in a function declaration, similar to most languages. Since no external parameter name is provided, the parameters need not be named at the call-site:

func join(s1: String, s2: String, joiner: String) -> String {
        return s1 + joiner + s2
}

join("hello", "world", ",");

However, an additional external parameter name, used for naming the parameter at the call-site, can be provided preceding the local parameter name. So in the following, the first parameter name is the external parameter name and the second is the local parameter name. Note that if an external parameter name is provided, it must always be used to name that parameter whenever that function is called. Also, external parameter names are not all-or-nothing; one parameter can be given an external name and another can opt-out.

func join(string s1: String, toString s2: String, withJoiner joiner: String) -> String {
        return s1 + joiner + s2
}

join(string: "hello", toString: "world", withJoiner: ", ")

Swift also supports sugar for easily defining the same name for both local and external parameter names using the # symbol as in the following:

// instead of
// func join(string string: String, toString toString: String, withJoiner withJoiner: String) -> String {
func join(#string: String, #toString: String, #withJoiner: String) -> String {
        return string + withJoiner + toString
}

Since Swift also supports default parameter values, it automatically provides external names for any parameter that has a default value, so that in the following, joiner is automatically made the external parameter name. This is a stylistic choice by Swift, in an effort to make the parameter's purpose "clear and unambiguous."

func join(s1: String, s2: String, joiner: String = " ") -> String {
    return s1 + joiner + s2
}

join("one", "two")
join("hello", "world", joiner: "-")

However, it's possible to opt-out of this automatic behavior by providing an explicit external parameter name of value _:

func join(s1: String, s2: String, _ joiner: String = " ") -> String {
    return s1 + joiner + s2
}

All that said, like I said in my first comment, perhaps this is overly complex for Rust's needs. Still, I thought it may be useful to compare to another recent/similar language that has tackled this problem.

@KokaKiwi
Copy link
Author

@blaenk
I really like the Swift's solution too, but I don't see how it could be integrated to Rust.
Maybe it could be integrated the same way as Switft: by adding external name before local name.
But I don't like the way Swift ask dev to specify "keywordable" arguments, I think all arguments should be (as Python does, in fact)

@blaenk
Copy link
Contributor

blaenk commented Sep 25, 2014

But I don't like the way Swift ask dev to specify "keywordable" arguments, I think all arguments should be (as Python does, in fact)

I think it's an artifact of Objective-C where both are required. The naming convention in Objective-C/Cocoa/etc. has named parameters like the above, toString and withJoiner, which are perhaps a bit unnatural to use within the function body, so they allow one to specify a clearer name.

From the RFC:

Modifying arguments names, because of named arguments feature (as we have to keep arguments names in function signature)

Since named arguments are part of the function signature, I imagine functions that use them won't be directly exportable as extern with the C ABI, for example, no? A wrapper function would need to be created that contains no named arguments, which itself delegates to the actual function with named arguments? Or will the compiler handle this case and simply strip the named parameters? This more or less maps to (but doesn't require AFAIK) Swift's local/external parameter names as well, since the compiler could simply ignore the external parameter names when externing. That said, I'm not too familiar with what externing is like in the compiler or anything.

@KokaKiwi
Copy link
Author

Since named arguments are part of the function signature, I imagine functions that use them won't be directly exportable as extern with the C ABI, for example, no?

As C ABI doesn't support default/named arguments, an extern "C" Rust function can't have default/named arguments.
I didn't write this in the RFC, but maybe it's as ambiguous case

@jsanders
Copy link

@P1start: Ah, good point. Seems like => is a decent compromise.

@blaenk
Copy link
Contributor

blaenk commented Sep 25, 2014

If we're already going to start bikeshedding over the symbol to use, I'm strongly in favor of the one proposed in this RFC, :.

@mahkoh
Copy link
Contributor

mahkoh commented Sep 25, 2014

mod M {
    pub struct X(int);

    pub fn f(x: X = X(1)) { }
}

fn main() {
    M::f();
}

The RFC doesn't say if this code is valid.

@KokaKiwi
Copy link
Author

@mahkoh Maybe I'm wrong, but I don't see any issue in this code.

@mahkoh
Copy link
Contributor

mahkoh commented Sep 25, 2014

@KokaKiwi The parameter in X is private.

@pnkfelix
Copy link
Member

@KokaKiwi you wrote:

I think it's "normal" if the compiler throw an error at call-time if the user update the function's library, as it's the same case if you change, for example, the name of a struct member used in a library.

but this is the point: the scenario you describe for struct only occurs for field names that are marked pub! The module author can locally rename non-pub struct names, and no client should know (apart from abstraction-breakage via e.g. deriving(Show)).

Someone who is writing a function may not realize that they do not have the freedom to choose different names later without risking breaking code downstream. Requiring keyword parameters to be marked pub makes this dependence explicit.

@pnkfelix
Copy link
Member

Closing as postponed; filing as part of RFC issue #323

@pnkfelix pnkfelix closed this Sep 25, 2014
@pnkfelix pnkfelix added the postponed RFCs that have been postponed and may be revisited at a later time. label Sep 25, 2014
@blaenk blaenk mentioned this pull request Oct 1, 2014
withoutboats pushed a commit to withoutboats/rfcs that referenced this pull request Jan 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
postponed RFCs that have been postponed and may be revisited at a later time.
Projects
None yet
Development

Successfully merging this pull request may close these issues.