[Pre-RFC] named arguments

This feature hasn’t even been accepted, let alone implemented.

1 Like
struct foo { x: i32 }
fn foo(pub x: i32) {}

fn main() {
    foo{x: 1};
    foo(x = 1);
}
1 Like

One of the point use = instead : to avoid confusing with named arguments.

fn quadratic(pub a: i32 = 0, pub b: i32 = 0, pub c: i32 = 0) -> Polynomial { 
    // ... 
}

// call as

quadratic(a = 0, b = 0, c = 0);

We could simply not have named parameters and actually pass a structural record (which is at a different type) and so it would be natural to reorder because that's how structural records work (and structs...): e.g. foo({ x: 1, y: true }).

6 Likes

FWIW, I had been working on a proc-macro to allow named & default parameters to be used. I have a general approach in mind that should allow it to work with generics in bare functions, methods in impl blocks and trait methods. I haven’t gotten around to documenting though. And I haven’t worked on it for several months.

The basic approach though is to generate a builder struct at a location relative to the function, and create-able through a nice named function (for top level functions, it’s implemented as a method in a module with the same name as the function, for a impl block methods and trait methods, it’ll be implemented as another method, with the name based on the function name). Then it provides a macro that translates a named syntax into builder calls, and then unpacks the builder values into the positional function call. And this is doable without requiring the end user of the methods to import anything extra (relative to the normal positional syntax).

It should emFWIW, I had been working on a proc-macro to allow named & default parameters to be used. I have a general approach in mind that should allow it to work with generics in bare functions, methods in impl blocks and trait methods. I haven’t gotten around to documenting though. And I haven’t worked on it for several months.

The basic approach though is to generate a builder struct at a location relative to the function, and create-able through a nice named function (for top level functions, it’s implemented as a method in a module with the same name as the function, for a impl block methods and trait methods, it’ll be implemented as another method, with the name based on the function name). Then it provides a macro that translates a named syntax into builder calls, and then unpacks the builder values into the positional function call. And this is doable without requiring the end user of the methods to import anything extra (relative to the normal positional syntax).

It should be able to enable the following calling convention:

  • {} is used to surround named arguments
  • The named arguments must come after the positional arguments
  • => in the named argument area
  • Something akin to struct sugar - {a => a} can be replaced by {a,}

I’d be happy to outline the plan better if anybody wants to pick it up and get it more implemented - currently the macro only works for bare methods without generics.

https://crates.io/crates/rubber_duck


Heck, if anybody to make a macro of this, but wanted to take a different approach to how the final syntax looks, or different call semantics, I’m more than willing to talk about approaches of how to implement it with proc macros (proc macros aren’t really a strength of mine, but I have spent some time trying to figure out how to abuse them for named/called syntax in a way that doesn’t require nightly).

Unless you’re referring to something different than what I think you’re referring to, structs require you to explicitly note their fields as pub for external consumers to be able to use their name. It seems fully consistent to me that you would have to explicitly mark parameters as pub to allow them to be used as named arguments.

I have encountered issues in the past where a Python package was intended to be a drop-in replacement for another package, but a third-party package was using named argument syntax to call the functions and they hadn’t kept identical parameter names between the packages. Hopefully being explicit about whether the parameter name is part of the public API would avoid issues like that.

4 Likes

I do not quite understand why it is proposed to use foo({ x: 1, y: true }) instead of just foo{ x: 1, y: true }. That is, without parens. I mean, this is what it is done with enum’s variants. Would removing the parens break something in this context?

1 Like

That would create an ambiguity between a function call and a struct literal.

Because a structural record literal expression would be { f0: x0, f1: x1, ... } and function application is foo(arg). There's no special logic here -- merely the composition of two orthogonal concepts. You could also write:

type MyRecord = { x: u8, y: bool };
let arg: MyRecord = { x: 1, y: true };
foo(arg);

One might imagine papering over the ( ) but I think that's not worth the special case for such a small gain in ergonomics.

2 Likes

I think that's the same situation as with function calls and tuple structs (foo(4) and Foo(4)), which apparently is fine, so I don't think there's actually a problem there.

1 Like

AIUI, the situation with tuple structs is that struct Foo(i32) actually does create both a type and a function named Foo. So are you suggesting record structs should do this too, or just that it’s syntactically OK for struct/call ambiguity to exist?

1 Like

I was thinking that if the solution worked for parens it could work for braces too. I don't know if it's the way we should go.

Yes, this is much better. Too much pub is quite ugly to look at.

fn get(url url: String, by timeout: u32, a a: i32, b b: i32, c c: i32){}
get(url: "https://", by: 32, a: 0, b: 0, c: 0);

is nicer than

fn get(pub url: String, pub timeout: u32, pub a: i32, pub b: i32, pub c: i32){}
get(url: "https://", timeout: 32, a: 0, b: 0, c: 0);

Hi, thanks for the proposal. I think using too much pub is quite ugly. Instead of:

fn new_window(pub title: &str, pub point: Point, pub width: u32, pub height: u32)

I like the swift way:

fn new_window(with title: &str, on point: Point, width width: u32, height height: u32)

Thank you.

pub looks better than repeating identifiers like width width and height height,and reads better due to highlighting.

I think pub should be used whenever the internal names match the external names,to avoid repeating the same identifier twice,only separated by spaces.Then if someone wants to change the internal name they can remove pub and add the new name after the old one.

One example of renaming the internal name:

before:

fn new_window(with title: &str, on point: Point, pub width: u32, pub height: u32)

after:

fn new_window(with title: &str, on point: Point, width w: u32, height h: u32)

Alternatively,we could just have a convention of not repeating the external name with the internal name.

3 Likes

That would be more consistent with the existing ergomics optimization for struct initializers:

let new_window = Window { with: title, on: point, width, height };

...and I really need to re-read this thread. I can't remember what's wrong with the Python approach of sacrificing the ability to have different external and internal names in the name of more readable function signatures.

1 Like

What I meant was that (as an alternative) we could require external and internal names when using named parameters,and avoid repeating the same identifier for the external and internal names in the function definition by convention instead of by using pub.

The 4 possibilities for how to declare named parameters that I can see (ignoring structs) are :

1 repeating the name:

fn new(width width: u32, heigth height: u32)

2 using pub

fn new(pub width: u32, pub height: u32)

3 using underscore

fn new(_ width: u32, _ height: u32)

4 a convention of not repeating internal and external names

fn new(width w: u32, height h: u32)

All of these would be the same to the caller:

let dimension=Dimension::new(width:10,height:20);

Ahh. My bad. I thought you meant making this valid...

fn new_window(with title: &str, on point: Point, width: u32, height: u32)

...and making it a convention to use the same names for internal and external.

Having read far enough back up, I've now managed to remind myself why my approach wasn't an option.

There is still the issue of destructuring pattern matching in functions args, for instance:

#[derive(Default)]
struct Interval { start: usize, end: usize }

impl Interval {
    fn width (
      Interval { start, end }: Interval,
    ) -> usize
    {
        end - start
    }
}

fn main ()
{
    let empty = Interval::
        default();
    Interval::width(empty);
}

Both this and the swift notation can be unified by using the @ sigil:

struct Interval { start: usize, end: usize }

impl Interval {
    fn width (
      // public_name @ binding: ty
      interval @ Interval { start, end }: Interval,
    ) -> usize
    {
        end - start
    }
}

fn main ()
{
    let empty = Interval::
        default();
    Interval::width(interval: empty);
    // or even
    Interval::width(interval@ empty); // solving the parsing ambiguity issue
}

For basic cases

fn foo (x @ x: usize);

some form of shorthand could be used, such as _@ to avoid repetition.

FWIW, named keyword arguments aren’t necesarily “free” when it comes to library maintenance. Keyword names are set in stone, even if newer versions of a library would like to make even backwards-compatible changes. For this reason, an entirely new syntax was recently accepted for Python PEP 570 to again limit their use in call sites (see link for details).

2 Likes