[Pre-RFC] Keyword arguments

  • Start Date: 2015-01-26
  • RFC PR #: (leave this empty)
  • Rust Issue #: (leave this empty)

Summary

Add keyword arguments to Rust in a backwards-compatible way.

Motivation

Allow another kind of argument in Rust. Current arguments distinguish themselves from other types by their order. If there is no semantic reason for a certain order, the argument order in functions is basically arbitrary. Consider a call like this:

window.addNewControl("Title", 20, 50, 100, 50, true);

First of all, the call tells nothing the user about what those values mean. Secondly, their order is arbitrary and must be memorized. What I propose is that this call would rather look like:

window.addNewControl(
    title => "Title", 
    xPosition => 20, 
    yPosition => 50, 
    width => 100, 
    height => 50, 
    drawingNow => true);

While you might argue that this is more verbose, a lot of libraries in JavaScript actually have a convention with calling with associative arrays like:

window.addNewControl({ xPosition: 20, yPosition: 50, width: 100, height: 5,
                 drawingNow: true });

If this wasn’t a convenient pattern, nobody would bother to do it.

An additional benefit is that this leads to an easy implementation of optional parameters and optional parameters. In languages like PHP, default parameters must come at the end of the function and must have a value. But in languages that support some sort of currying, not specifying the last parameter gives you a partially-applied function! This means that default parameters and currying/partial application are at odds with each other normally because you can’t tell when a function is partially-applied or has default parameter.

The design with keyword arguments doesn’t have this problem: you can execute the function when the last positional argument is given and list all the keyword arguments out of order. That means you can keep passing arguments to a partially-applied function and it won’t execute until the last positional argument is given. Furthermore, you can have optional parameters in the beginning, middle and end of functions without any restrictions other than that they have to be keyword arguments.

Detailed design

Currently Rust has

fn slice(&self, begin: usize, end: usize) -> &'a str
fn slice_from(&self, begin: usize) -> &'a str
fn slice_to(&self, end: usize) -> &'a str

This can be changed to

fn slice(&self, from => begin: usize, to => end: usize) -> &'a str
fn slice(&self, from => begin: usize) -> &'a str
fn slice(&self, to => end: usize) -> &'a str

Note that these are three different functions that have three different signatures. The keywords a function accepts is part of its signature. You can call these functions like this:

foo.slice(from => 5); //equivalent to current foo.slice_from(5)
foo.slice(to => 9);   //equivalent to current foo.slice_to(9)
foo.slice(from => 5, to => 9);       //equivalent to current foo.slice(5, 9)
foo.slice(from => 9, to => 5);       //equivalent to current foo.slice(5, 9)

if Rust had some kind of way to curry functions added later, you could curry the arguments and then manually pass the &self as the last argument to the function:

let x = curried_slice(from => 5); //curried
x(foo); //foo.slice_from(5);
let y = curried_slice(from => 5); //curried
let z = y(to => 10); //curried
z(foo); //foo.slice(5, 10);

So this feature is future-proof for a possible currying method in Rust. In JavaScript a lot of libraries cannot be used by some kind of a curry() function because the order of the arguments is wrong - the authors never considered that someone would try to curry their functions and partial application in JS is pretty useless because of this (you sometimes want to partially apply the SECOND argument and it’s painful to do this). Having a design that already solves issues with argument order is an indirect benefit.

Drawbacks

This is a more complicated design than just having default arguments and overloading. Now there are two different types of arguments (positional and keyword) and they might interact with each other, lifetimes, traits, closures in different ways. It doesn’t solve the problem of currying when there are no positional arguments in the first place, so you need at least one positional argument to curry in a pain-free way.

Alternatives

A better design to the above function might be designing it like so:

let title = "Title";
let position = Position(20, 50);
let dimensions = Dimension(100, 50);
window.addNewControlDrawingNow(title, position, dimensions);

Now the function takes three parameters, and we assigned them meaningful names. We have created two different functions addNewControl and addNewControlDrawingNow instead of passing a boolean to choose whether the control draws now.

While this design is better, it still doesn’t solve the problem of having to remember of what order to put dimensions, position, and the title. At least the compiler will now verify that the types are correct. It is still up to the programmer to actually name those variables well, instead of the API specifying what the keywords should be.

If keyword arguments themselves are not implemented, then there’s also the issue of overloading to enable better API design.

Unresolved questions

Should closures have keyword arguments as well?

This actually sounds like how Objective-C’s selectors work (the “keyword argument names” are part of the method name and are compulsory).

Questions:

  1. Are the keyword arguments unordered? Is foo.slice(to => 5, from => 0) valid?
  2. Can keyword arguments be mixed with normal arguments? fn foo(a: i32, b: i32, c => d: i32)?
  3. How do I take a function pointer from these functions?
    • What is the type of the function pointer?
    • If the function pointer type is not something simple like fn(i32, i32) -> i32, what is the representation of the corresponding Fn/FnMut/FnOnce trait, after desugared into Fn<A, Result=R> form?
  4. [Bikeshed] Why => is chosen? Note that println! uses = for named parameters. : cannot be used as it is reserved for type ascription.

I suggest “currying” / partial application be taken out from the RFC, it is orthogonal to the concept of keyword arguments.

1 Like
  1. I would prefer unordered so I don’t have to remember the exact order.
  2. Yes, this is part of the design for optional arguments, the optional arguments will be keyword arguments and can be listed in any order (not just in the end).
  3. If you want to have that, the type of the function pointer will be fn(i32, up_to => i32) -> i32 which will not type check if you passed in a fn(i32, i32) -> 32. Its internal representation is the same, but it’s a different type.
  4. This is because the design for default values of these parameters uses =
fn slice(&self, from => begin: usize = 0, to => end: usize = self.length)

or maybe even

fn slice(&self, from => begin = 0us, to => end = self.length)

Currying/partial application is orthogonal, but alternative designs like default type parameters make currying impossible from the end, and doing it from the beginning does not conform to expectations. This design solves that issue if it were ever to come up.

Regarding 3, I mean that currently Fn(A, B, C) -> R will desugar into Fn<(A, B, C), R>. Then what should Fn(A, foo => B) -> C become? This is actually about that unresolved question “should closures have keyword arguments as well”.

1 Like

Why add additional keywords? The argument names are already there. Why not use those? Then any function would be callable this way, the order would be found by the compiler at the callsite.

bikeshedding: the => operator reminds a lot of matches, instead do it like structs do it:

struct SuperArgs {
    cake : i32,
    colors : String,
    nothing : Option<i8>,
}
fn potential_future_func(colors : String, cake : i32, nothing : Option<i8>) {
    println!("{},{},{:?}", colors, cake, nothing);
}

fn super_func(args : SuperArgs) {
    println!("{},{},{:?}", args.colors, args.cake, args.nothing);
}

fn main() {
    super_func(SuperArgs{cake : 99, colors : "hi".to_string(), nothing : None});
    potential_future_func(cake : 99, colors : "bye".to_string(), nothing : Some(42));
}
5 Likes

Keyword arguments also give you the ability to overload functions by keyword argument.

So in fact I can have a function like:

fn slice(&self, to => end: usize) -> &'a str vs. fn slice(&self, from => begin: usize) -> &'a str

Notice that the signatures of these functions are both usize -> &'a str which is why they would still need a different name even if Java-style overloading would be added to Rust.

you are trying to do two things in one rfc. overloading and keyword arguments. I suggest you stick to the latter, as overloading could be added later (probably even backwards compatible).

4 Likes

As written above, : is will interfere with type ascription. Unless the names are compulsory, we should better not use :.

not quite, since you don’t add types in function calls, but only in function definitions

2 Likes

Variable namespace and type namespace can have overlapping.

use std::default::Default;

#[allow(non_camel_case_types)]
#[derive(Default)]
struct u128 {
    pub x: u64,
    pub y: u64,
}
fn foo<T>(u127: T) {
    /* what is type T if we allow both keyword args and type ascription? */
}
fn main() {
    let u127 = Default::default();
    let u128 = "1234";
    foo(u127:u128);   // <--- note that foo(u128) works today.
}

as I said, in function definitions there’s a type after the colon. in functions calls (and today already in struct creation) it is an assignment/initialization.

Therefore in http://discuss.rust-lang.org/t/pre-rfc-keyword-arguments/1453/8?u=kennytm I said unless the names are compulsory these two features will interfere if : is chosen. The keyword argument names must be optional for 1.0-compatibility. You can write

fn potential_future_func(colors: String, cake: i32, nothing: Option<i8>) {}
potential_future_func("bye".to_string(), 99, Some(42));

today, and this should continue to work before 2.0. Struct creation work because the field names are compulsory in the expression.

I don’t see how the following (including both named and positional arguments at the same time) would be incompatible to 1.0. Right now, colons in function calls don’t have any meaning, therefore they can be added in the future.

fn potential_future_func(colors: String, cake: i32, nothing: Option<i8>) {}
potential_future_func("bye".to_string(), nothing: Some(42), cake: 99);

If you mean the keyword-renaming( and overloading), then I agree, the renames can’t be separated from argument name and type by the colon operator in the function definition.

Please check https://github.com/rust-lang/rfcs/issues/354.

thanks, didn’t know about expression type ascription.

Well if I can’t talk about optional arguments/overloading/currying, then there are no benefits to keyword arguments over just passing in a struct!

I am talking about overloading and currying because they are the benefits of this design over just slapping on default arguments and calling it a day.

There are two options that I see, which aren't mutually exclusive. One is to ensure that tuples mirror any feature you add to function signatures. So Fn(a: A, foo => b: B) -> C would desurgar into Fn<(A, foo => B), C>. The other possibility is to have a fully-specified syntax for keyword functions, like func(=>foo) which is assignable to Fn<(A, B), C>.

I am not necessarily opposed to this, but as for your example, note that you can do this:

struct ControlArgs<'t> {
    pub title: &'t str,
    pub x_pos: i32,
    pub y_pos: i32,
    pub width: u32,
    pub height: u32,
    pub drawing_now: bool
}

fn add_new_control<'t>(args: ControlArgs<'t>) { ... }

window.add_new_control(ControlArgs {
    title: "Title",
    x_pos: 20,
    y_pos: 50,
    width: 100,
    height: 50,
    drawing_now: true
});

Which for the caller is almost identical to the Javascript version and your proposed syntax. It lets you provide named arguments in an arbitrary order.

2 Likes

Well if I can't talk about optional arguments/overloading/currying, then there are no benefits to keyword arguments over just passing in a struct!

Wouldn't overloading be also possible with a struct? I think the overloading shouldn't in any way depend on the keyword arguments and vice versa, they are different concerns. It would've been counter-intuitive if oveloading somehow depended on keyword arguments instead of being simply the general overloading like in other languages.

Overloading based on the keyword arguments looks like an interested idea, but why should it be special? Wouldn't people abuse it by emulating the normal overloading with keyword arguments, e.g. introducing unnecessary bloat by writing foo (array => []); foo (map => my_btree_map) instead of simply passing foo ([]); foo (my_btree_map)?

On the other hand, maybe the normal overloading isn't going to lend in Rust (any time soon), making the keyword argument based overloading desirable as a poor man's alternative?

As for benefits of keyword arguments, with a struct you can't use the positional arguments, you are forced to name everything, not to mention that it bloats the program's namespace and looks less ideomatic and takes the choice out of the hands of the caller. So the keyword arguments make a lot of sence even without the overloading, one can make a more readable code with them where and when it's necessary and without a need to modify the API. Thus, a concern I have with this pre-RFC is that it would clash with arguably better keyword arguments proposals, like this one https://github.com/rust-lang/rfcs/pull/257.

1 Like

In my opinion (which I’ve explained before, not that anyone cares), freeform overloading is bad, keyword and optional arguments are good. Objective-C like mandatory keywords are nice for self-explanatory code, at least if you have an IDE, but very different from regular keywords+optionals, because they only achieve the same purpose (allow you to omit arguments) if you add overloading. (You’ll notice that most Objective-C methods are highly verbose and take a lot of arguments…) Python-like keywords+optionals are a good way to not have to create a bunch of helper methods to do the same thing with slightly different variations on how much is specified, but that’s exactly what overloading forces you to do.

However, Python’s system has problems, especially related to the propagation of arguments and defaults to wrapper methods. I think my preferred system would basically be a way to mark a function so that it automatically creates an argument wrapper struct when called (which can then be passed directly to other functions without re-specifying each argument, etc…).

1 Like