[Pre-RFC] named arguments

What about traits?

fn bar<F: TraitFoo>(pub f: TraitFoo = Foo::new()){}

I guess it would be Fn(u32, u16, u8) since it should be compatible with the positional form. But I am not sure what the implications would be. This is reaching the limit of my knowledge of Rust and the compiler :slight_smile:


I added another alternative proposed in the discussion on the tracking issue. For better exposure I will paste it here too.

Parameters trait

Add a special trait, for example Params: Default and have syntactic sugar for invoking functions that have their last argument implementing Params.

Also add syntactic sugar for automatically defining an anonymous params struct.

For example:

#[derive(Params, Default)]
struct ExplicitParams {
    herp: u8,
    derp: i8,
}

// Use this form when you have many parameters
fn explicit(durr: &str, named: ExplicitParams) { ... }

// Use this from when you only have few parameters
// The params struct is automatically defined for you which takes away a lot of the verbosity
fn implicit(foo: &str = "xy", bar = 1u8) { ... }

fn main() {
    explicit("unnamed", derp = 2);
    // desugared: explicit("unnamed", ExplicitParams { derp: 2, ..Default::default() })

    implicit(bar = 2);
    // desugared: implicit(AnonymousStruct { foo: "xy", bar: 2 })
}

This would also take care of default arguments at the same time!

Note that the ambiguity with type ascriptions only exists if the expression being ascribed consists of a single identifier (referring to a variable or constant), as an argument name is always a single identifier. Cases like

func(foo: ...)

But there is rarely a need to write a type ascription of a variable/constant, or a reason why one would do so by mistake. Since type ascription is not stable, the parser could be changed to carve out the special case of ā€œidentifier followed by colon in argument positionā€ for keyword arguments, and parse as type ascription in all other cases.

Type ascription can do two things: affect type inference of the value on the left, and perform implicit coercions. For the former, since the type in question is the variableā€™s type, it would make much more sense to just put it on the variable declaration. The latter will usually happen automatically. There are exceptions, like if @ubsanā€™s example is changed to use a variable:

fn print<T: std::fmt::Debug>(t: T) { println!("{:?}", t); }
fn main() {
    let r: &i32 = &0;
    print(r: *const i32);
}

ā€¦but I donā€™t think theyā€™re common at all. (By the way, current nightly doesnā€™t like this example with #![feature(type_ascription)] - it errors with a type mismatch - but based on the RFC I think it should be allowed.) If you encountered such an ambiguity, you could fix it just by surrounding the whole thing in parentheses ā€“ print((r: *const i32)) ā€“ or alternately by changing : to as (though that might produce a warning).

Also, there arenā€™t that many implicit coercions, and with the raw pointer ones, the parser could have a special diagnostic: *mut and *const cannot be the start of an expression, so the fragment func(identifier: *mut or func(identifier: *const could produce an error explicitly stating that parentheses are required. (Or it could just automatically disambiguate as an ascription, though thatā€™s probably more confusion than itā€™s worth.) Similarly, the current parser error that tends to crop up if you write a generic type where itā€™s expecting a value:

error: chained comparison operators require parentheses
 --> <anon>:6:18
  |
6 |     foo(&Iterator<i32>);
  |                  ^^^^^^
  |
  = help: use `::<...>` instead of `<...>` if you meant to specify type arguments

could be changed to note cases where a type ascription might have been intended instead of a keyword argument.

2 Likes

I donā€™t have a concrete suggestion right now, so some thoughts:

  • I like the idea of named arguments
  • For once it is a feature with clear semantics, but tricky syntax
  • There are lots of symmetries to consider - there are clear similarities between function arguments and tuples, and thus between named arguments and struct fields. There are also correspondences between the way we treat tuples and structs, and in how we treat patterns and function arguments. All of these are partial, however. We do not have any concept of ā€˜mixedā€™ data types in Rust (i.e., a struct with some anonymous and some named fields), although the idea has come up in some discussion of nested enums.
  • Iā€™m not sure if I like the pub notation or not - it kind of makes sense, but is also something of a stretch. In particular pub(restricted) makes no sense in this context.

(Once again, I also which we could distinguish between values and types syntactically - it would make life so much easier, but probably uglier).

5 Likes

Iā€™m against using : because of the parsing ambiguity mentioned. There are similar issues with =, and something like foo(x => y) seems unappealing.

Has anyone mentioned using anonymous structs? If you just allow anonymous structs, like we can write (x, y, z) instead of Type(x, y, z), in both pattern and expression position, then the API can be

// Definition
fn free_fall({z0, v0, g}: {f64, f64, f64}) { ... }

// Call
free_fall({z0: 100.0, v0: 0.0, g: 9.81});

I would add two extensions, but they arenā€™t needed for a first prototype:

  1. Let the function definition put the types inside the anonymous struct:

     // old
     fn free_fall({z0, v0, g}: {f64, f64, f64}) { ... }
    
     // new
     fn free_fall({z0: f64, v0: f64, g: f64}) { ... }
    
  2. Just like you can ā€œautomaticallyā€ unpack a pattern with let Type { x, y, z } = ... rather than let Type { x: x, y: y, z: z }, make it possible to automatically pack with Type { x, y, z } as an expression to mean Type { x: x, y: y, z: z }. This prevents the only case I actually consider ugly:

     // old
     http.get("https://www.rust-lang.org/en-US/", {timeout: timout});
    
     // new
     http.get("https://www.rust-lang.org/en-US/", {timeout});
    

This proposal requires very little new. You miss out on the ā€œseamless upgradeā€, but I think this claim is seriously overestimated in importance. Right now, people write builders when named arguments would make sense, and thus most APIs donā€™t need to be upgraded. When youā€™re writing a function, itā€™s generally clear whether it would benefit from named arguments.

Itā€™s worth noting that this gives a clear path to defaults, by providing syntax sugar for

    http.get("https://www.rust-lang.org/en-US/", {..HTTP_GET_DEFAULTS});

Note that because these are anonymous types, one can reasonably allow HTTP_GET_DEFAULTS to be a ā€œsubtypeā€ of the actual default arguments.

2 Likes

What if we made the user write parentheses in this case? The call would become print((&0: *const i32)).

2 Likes

Some more thoughts and a sort-of proposal:

  • Due to the analogy with fields in structs, I think it is essential to use : for specifying the actual args
  • This has parsing issues, so we need some kind of opt-in in a function use
  • Currently, we have special rules around braces in multiple places in parsing to deal with the same. Iā€™m not sure of any other similar places we do this
  • We canā€™t use f{ā€¦} as calling syntax due to confusion with struct ctors (functions and structs are in different namespaces, so if the is a function and struct with the same, we canā€™t tell which operation is intended).
  • I think we need to consider defaults at this stage to make sure the syntax is extendable in that direction.
  • If we allow mixing of named and anonymous args, then the anonymous ones must come before the named ones, and must be in order (this is not actually a hard constraint, but life gets pretty messy otherwise).

So, my sort-of proposal (which is similar to some other suggestions above) is to separate named arguments into their own block, indicated with braces and which comes after other arguments in both declaration and use. The correspondence with anonymous structs should be obvious, but I am not proposing adding these to the language in general.

Examples:

// Named and un-named.
fn foo(x: i32, { y: i32, z: i32 }) { ... }
foo(42, { z: 0, y: -1 });

// Only unnamed
fn bar({ x: i32 }) { ... }
bar({ x: 42 });

// Defaults:
fn foo(x: i32, { y: i32 = 32, z: i32 }) { ... }
foo(42, { z: 0 });

Downsides:

  • kinda ugly, especially if there are no un-named args.
  • what does fn foo(x: i32, {}} mean?

Parsing:

  • in the decl we parse the { ... } as a pattern, and if there is no type following, treat it as a ā€˜named argument blockā€™
  • in a use, we must distinguish between a block and an expression. This is somewhat awkward. I think the best way to handle this is to add the anonymous struct syntax to the grammar of expressions. We would ban using a type ascription expression as the first expression in a block to remove ambiguity (maybe only type ascription of a variable). If this anon struct syntax is used anywhere other than in a function call, it is an error. In a function call it is always a named variable block.

Function types:

I think the syntax extends easily to function types, but only if we allow anon structs outside of function calls. Without that, I donā€™t see a ā€˜niceā€™ way forward. But we could do something hacky with named structs and an attribute or something + some extra sugar for the () generics notation.

Random thought:

I wonder if we could use a similar syntax (anon struct) in the declaration of trait fields (RFC 1546). That syntax with struct was suggested in the discussion thread there.

2 Likes

That seems no better than saying "don't use type ascription in function calls". If we do this in terms of anonymous structs, it makes sense to disambiguate the same way we disambiguate (x) from (x,): require writing {x: y,} instead of {x: y}.

I think it is different (though reasonable people could differ on how different):

  • we have lots of special parsing rules around braces already, but not so much around parens,
  • it will impact less code - there are lots of function calls, there will be far fewer anon structs.

The trouble with the disambiguating comma is that we currently use it to inform the parser that the surrounding expression should be treated as a tuple rather than a delimited expression. However, doing this for anon structs feels like the wrong default - it seems like using type ascription in a block in a function call would be super-rare, so making that the default and requiring a comma for the far more common operation of naming an arg, seems backwards.

What about just requiring a keyword to begin the named arguments. Then, we can reliably switch context.

fn foo(x: i32, use y: i32, z: i32) { ... }
foo(42, use y: -1, z: 0);

Iā€™m not a fan of use on the basis that itā€™s doing something ā€œunnaturalā€ relative to what it currently means, but it does solve the parsing (both compiler and human) issues, as far as I can see. Or, we could steal a trick from varargs:

fn foo(x: i32, ..{y: i32, z: i32}) { ... }
foo(42, ..{y: -1, z: 0});

This could also later be adapted for ā€œexplodingā€ tuples for varargs. Combine with variadic tuples and anonymous structs, and youā€™ve got something pretty extensible and powerful. This is a little ugly, though, and might warrant some additional shortcut:

fn increase(..{by: i32}) { ... }
increase(..{by: 3});

I have no idea what this would mean for types.

1 Like

I assume that should be a : not a =?

it should yes, Iā€™ll edit

Y'know, it could be made to make sense. What if pub on arguments was only required to use them as keyword arguments from outside the module? Within a module, it would be allowed freely, since that doesn't create the same backward-compatibility hazard - which also means reduced clutter, since non-pub fns would never need to use pub on their arguments.

Edit: Unless that's what everyone was assuming anyway. (I read the original RFC as always requiring pub for keyword arguments.)

3 Likes

Currently I donā€™t have a significant need/desire for named arguments in Rust (but perhaps Iā€™d like default arguments).

Named arguments were discussed many times for D language, but in the end they were refused (even if in D the syntax for type ascription is different and doesnā€™t clash the most natural syntax for named arguments).

Ada language uses the ā€œ=>ā€ syntax: Foo (Arg_2 => 0.0, Arg_1 => 1);

Here you see some alternative syntaxes: https://rosettacode.org/wiki/Named_parameters

In Scala: http://docs.scala-lang.org/sips/completed/named-and-default-arguments.html

To face the problem of freezing argument names in the API, Scala uses this: def inc(x: Int, @deprecatedName('y) n: Int): Int = x + n

See: http://lampwww.epfl.ch/~hmiller/scaladoc/library/scala/deprecatedName.html

Named arguments is not near the top of the list of features I miss in Rust.

2 Likes

Of all the options presented so far, I strongly prefer using func(x: y) as the call syntax, which as @comex points out, could be done by having the parser recognize an identifier followed by a colon as a named argument, and otherwise parse as an expression. To disambiguate the example, I would probably write print((r): *const i32) instead of print((r: *const i32)), though obviously both would work.

I find this idea intriguing. (I read the RFC the same way you did.)

3 Likes

What if you want

struct A{
    a: u8,
    b: bool
}
fn foo(pub A{a:c, b:d}) -> u16{1}

In wich ways can you call it?

foo(A{a:10, b:true});
//or
foo(A{c:10, d:true});
//or even
foo(A{a:10, c:true});

Or are patterns forbidden for named arguments?

It is already possible by:

fn foo(A { a, b }: A) -> u16;
1 Like

I mean do you have to use the struct field names (a,b) or the pattern bind names (c,d) as names for the arguments?

You can https://is.gd/CTZRZp

struct A{
    a: u8,
    b: bool
}

fn foo(A { a: c, b: d }: A) -> u16 {
    println!("{:?} {:?}", c, d);
    
    42
}

I believe Swift has a shorthand for this case (_ x or something). I'm pretty well persuaded that one ought to separate the "external name" from the "internal name" -- I found the increment example alone pretty persuasive. Maybe it's just because I like smalltalk. I also like that it provides a natural form of opt-in.

Regarding the parsing ambiguities, I definitely agree that foo(x: y) is the natural syntax. It's unfortunate that it collides with ascription though -- I really don't want to require parentheses to disambiguate, that is basically always regrettable.

I am wondering though if changing ascription syntax isn't a better choice. =) It's isn't stable, after all. Just something to consider. :slight_smile:

13 Likes