Idea for named function arguments and symmetry with structs/enums

I was thinking that we should support syntax like func{a: 456, b} where func is a function where it's entirely analogous to constructing a struct or enum variant Struct { a: 456, b }. This is symmetrical to how tuple structs/enum-variants are analogous to functions and can even be used as functions. This would solve most of the issue with wanting named function arguments and make Rust more symmetrical in the process. Basically, all struct/enum-variant constructors (both tuple and not) would end up being just special functions, rather than being a whole different kind of expression. This would include being able to call a tuple function (where the argument names are internal details) fn my_func(a: i32, b: f64, c: &str) {} using struct syntax: my_func { 0: 12i32, 1: 3.4f64, 2: "blah" } since that's how you'd call a tuple struct's constructor. You'd also be able to declare a non-tuple function (where the argument names are significant) fn my_fn{my_i32: i32, my_f32: f32} {...} and call it, just like a non-tuple struct constructor (note changed argument order): my_fn{my_f32: 4.5, my_i32: 42}.

What do you all think?

3 Likes

Given that people seem fine with foo(1, 2) (normal function) vs Foo(1, 2) (tuple struct construction†) hasn't caused serious problems, it seems like foo{a: 1, b: 2} vs Foo { a: 1, b: 2 } would be fine too.

Name resolution is a hard part here to me. You can define fn Foo(a: i32) {} and struct Foo { a: i32 } at the same time, so I'm not sure what's supposed to happen if you make fn Foo{a: i32} {} -- which of those other two things are a name conflict with it?

There's also a bunch of questions about whether they can be passed to things that want Fns, whether there's a way to say that a parameter is a function that takes named parameters, etc.

And of course there's the way that works right now of just having it be func{a: 456, b}.make_it_so() by having struct func {a: i32, b: i32} and impl func { fn make_it_so(self) -> ... { ... }...

† Technically just a function call too.

I'd say fn Foo{a: i32} {} should conflict with both fn Foo(a: i32) {} and struct Foo { a: i32 }, since they all would be usable as values, and we can just have fn Foo(a: i32) {} override struct Foo { a: i32 } in the value namespace with a warning for backwards compatibility? Both struct Foo { a: i32 } and fn Foo{a: i32} {} would be function literals (or whatever the actual name is), for struct Foo, Foo's type is fn{a: i32} -> Foo and, for fn Foo{..., Foo's type is fn{a: i32} -> () (return type not implicit for clarity).

Fn types could look like Fn{a: i32, b: String} -> i64, function pointers would be fn{arg1: i64, arg2: u8} -> usize, and passing a generic function with named args would be fn my_fn(f: impl Fn{named_arg: u64}) { f{named_arg: 4}; todo!() }

That would keep working just fine, since func would be both a type (so it can have impl's) and a function literal value of type fn{a: i32, b: i32} -> func:

let v: fn{a: i32, b: i32} -> func = func;
let v: func = v{a: 456, b};
let v = v.make_it_so();

To clarify, both declare Foo as a value that is a function literal.

As I understand it, there's a type namespace (structs, enums, traits), a value namespace (variables, consts, functions), a macro namespace and a lifetime namespace. If the rules are explicitly defined, I don't know where. There are some interesting quirks...

Various examples of naming conflicts and shadowing

No overlapping function names or struct names:

fn val() {}
fn val(b: u8) {}
// Error: `val` must be defined only once in the value namespace of this block
// (same for two structs, or a struct and a function, with the same name)

This will bite you if you name a function the same as a tuple struct type (since that creates an entry in value namespace as well, for the constructor):

struct TupleStruct(usize, isize);
fn TupleStruct(f: f32) -> bool { false }
// Error: `TupleStruct` must be defined only once in the value namespace of this block

Variables can shadow function and struct names though (even if the variable comes first!):

fn main() {
    let val = 5;
    fn val() {}
    val();
    // Error: call expression requires function
}

However there's an explicit exception for tuple struct constructors!

fn main() {
    struct TupleStruct(usize, isize);
    let TupleStruct = 5;
    // Error: cannot be named the same as a tuple struct
}

And you can't shadow a const...

fn main() {
    const Foo: bool = true;
    let Foo = -2;
    // Error: `Foo` is interpreted as a constant, not a new binding
}

And the case of unit structs, instead of a fn constructor you get a const (I believe; the error message does distinguish it as a unit struct).

fn main() {
    struct Unit;
    let Unit = -3;
    // Error: `Unit` is interpreted as a unit struct, not a new binding
    // (`fn` gives the same "value namespace" error as with normal or Tuple structs)
}

Finally as @scottmcm said, you can create a (non-tuple, non-unit) struct and fn with the same name currently, as they are in difference namespaces (so this would be a breaking change... or confusing with a new namespace I suppose):

struct StructStruct { a: usize, b: isize }
fn StructStruct(_: f64) -> () {()}
// compiles

While I do understand the idea and motivation, the proposed syntax is IMHO getting too close to a C++-like initialization syntax and the accompanying confusion of "wait, when do I use parentheses and when do I use braces, and what's the difference exactly?".

For named arguments (which I'm inherently sceptical of) I'd much prefer the "anonymous struct parameter" approach from prior threads on the topic (exact syntax details TBD), esp. with defaults elision:

let foo = bar(positional_1, positional_2, _ { named_1: x, named_5: y, .. });  
4 Likes

I wonder how similar questions are solved in Swift. I heard there argument names are effectively treated as part of function name. But I don't know any further details.

  • is the order of arguments mandatory in Swift?
  • how exactly can unnamed arguments be mixed with named arguments in Swift?
  • how are they dealing with function type question?

Update: the book doesn't seem entirely clear.. Well function types at least in Swift are same as function types in Rust - they don't have any parameter labels. It seems argument order is enforced in Swift even if arguments are named. I'm not entirely clear if you can skip argument 3 (allowing it to go default) while still supplying argument 4.

Function name overloading is running galore in Swift so I'm not entirely clear either what disambiguation rules are when parameters with default values are skipped.

1 Like

The idea is the form with {} is the fully general form and () is just conveniant syntax sugar for the tuple case -- where arguments are named 0, 1, 2, ...

Luckily this is not C++ with it's multiple forms that often end up calling different functions when combined with function overloading... std::vector(4, 5) and std::vector{4, 5} are confusing (like Rust's vec![5i32; 4] and vec![4i32, 5i32] respectively).

In Rust, calling using the wrong arguments usually is a type error.

This proposal would transform all tuple function declarations:

fn my_fn(a: i8, b: i16, c: i32, d: i64) -> isize {
    todo!();
}

to syntax sugar for (similar to tuple structs):

fn my_fn{0: i8, 1: i16, 2: i32, 3: i64} -> isize {
    let (a, b, c, d) = (r#0, r#1, r#2, r#3); // variables with names 0, 1, 2, and 3
    todo!();
}

It would transform all tuple function calls:

let result = my_fn(5i8, 10i16, 15i32, 20i64);

to syntax sugar for (similar to tuple structs):

let result = my_fn{0: 5i8, 1: 10i16, 2: 15i32, 3: 20i64);

I haven't quite figured out what to do about self parameters, maybe:

fn my_fn(self: MyType, a: i32) -> i8 {
    if a == 5 {
        return 3;
    }
    self.my_fn(5i32);
    my_fn(MyType{}, 5i32)
}

would be syntax sugar for something like:

fn my_fn{#[self] 0: MyType, 1: i32} -> i8 {
    let (self, a) = (r#0, r#1);
    if a == 5 {
        return 3;
    }
    
    // note `self.my_fn{1: 5i32}` skips arg 0 since that is used by the `self` arg
    self.my_fn{1: 5i32}; // or: `my_fn{0: self, 1: 5i32}`
    my_fn{0: MyType{}, 1: 5i32}
}

Yes

A function's full name* is of the form: baseName(argName1:argName2:) etc.
some of the name components can be keyword name components, eg. initializers have the base name init, likewise "unnamed" arguments actually have the keyword name _.
if you want to use a name that would typically be interpreted as a keyword name in that position, you can use grave accents like `keyword`, e.g. `init`.

* due to source compatibility, one can often just use the base name, which has led to problems, see here.

None of a function's name is part of its type.

In addition, Swift has two namespaces the label namespace, and the everything else namespace. (I wish it had only one).
The label namespace is function scoped, and the other is scoped by everything:
the closest defined thing with the first name in a path is found, and then you descend its named sub-scopes for the rest of the path.

Thanks for confirming! There are still things I haven't quite figured out about functions in Swift.. Arguments with default values are by their nature optional

  • they don't form part of function name then?
  • do all arguments with default values have to come after all arguments without?
  • does anything interesting happen when a function with default arguments is assigned to a variable of a function type which has a shorter list of arguments?

You get "error: cannot convert value of type (Int, Int) -> Int to specified type (Int) -> Int". (There exists an e.g. foo(x:y:) but no foo(x:), even if y: has a default. I'm fairly certain the default is just always provided by the caller.)

That said, Swift does do a lot of similar reabstraction when passing functions around, so introducing a reabstraction thunk here wouldn't be out of character.

they do, see @CAD97's post above.

No it is possible to have an arbitrary mixing of defaulted and non-defaulted arguments.
IIRC, it is possible to define a function which has defaulted arguments with some defaults that cannot ever be used, but I don't remember the precise rules, and so cannot give examples.

IIRC, The implementation of default arguments is that, for each argument with a default, an LLVM function is created e.g. getFooDefaultArg1() (but mangled), which is implicitly called as a parameter of the original function, when the default is used.

Hmm.. now it has started being a bit confusing.. if arguments with default values are part of function name it means that func foo(a : Int) can co-exist with func foo(a : Int, b : Int = 12) and then we either cannot invoke the 1st or cannot use the default argument value for the 2nd... Unless some funky syntax like foo(a : 4, b : _) exists.

The point obviously was that Rust potentially could do some of what Swift is doing. I suppose allowing overaloaded functions that differ only in the number of arguments would be way too confusing. But otherwise treating public argument labels as part of function name could work.. I suppose the rule could be: concatenate all named arguments (using a suitable separator) and implicitly append them to function name. Clashes between function names such composed wouldn't be allowed so that fn foo(pub a : i32) would be allowed to co-exist with fn foo(pub b : i32) but not with fn foo(pub a : i32, b : i32). It is still an interesting question how to distinguish the first two when coercing to an Fn trait (function type).

If arguments with default values were also allowed the question of including them or not including into function name (for the purpose of allowing controlled function name overloading) would need to be answered.

Of course this all is probably discussed approximately once a month year after year :slight_smile:

Oh, right that's one of the simple cases of hidden defaults:
In that case the one without defaults is called.

Yes. I have personally interacted with 3 other threads on this:

[Pre-RFC] Named .arguments
[Pre-RFC] named arguments
Named arguments increase readability a lot

And some I haven't:

Possibly one small step towards named arguments
Yet Another named arguments prototype

This has already been discussed. I will try to sum-up what was said. I also added two new points: #[non_exaustive] and #[unstable] arguments.

  • This need to be opt-in, because of semver implication. The identifiers of the fields (just like identifier for struct fields) are part of the public API. A library author should be able to change the name of the parameter of a function that he didn’t intent to stabilize.

This can be easily solved by using curly braces at the declaration site to express the intent of the author to allow both positional and named usage.

Declaration:

// named argument + positional argument
fn f1{foo: i32, bar: usize} -> String;
// positional argument
fn f2(foo: i32, bar: usize) -> String;

Usage:

// f1 was declared with named argument syntax
let s1 = f1{foo: 18, bar: 21}; // named argument -> OK
let s1 = f1(18, 21); // positional argument -> OK

// f2 was declared with positional-only syntax
// let s2 = f2{foo: 18, bar: 21}; // doesn’t compile
let s2 = f2(18, 21); // positional argument -> OK

This also solve the issue of namespace clashed between struct Foo { ... } and fn Foo { ... } since current code will not "magically" be using the named argument syntax. A warning could be added against declaring both struct Foo {...} and fn Foo(...).

  • Open question: should the name of the argument be part of the type of the function? ie. should fn f1{foo: usize} share the same type as fn f2{bar: usize}? What would be the syntax of a named function pointer, and the associated Fn traits.

I think that both could be implicitly coercible into Fn(usize) if a named function can also be called using positional syntax.

  • This syntax doesn’t support calling a function with both positional + named argument

In python, you can specify if an argument is positional only, positional + named or named only:

def foo(x1, x2, /, y1, y2, *, z1, z2)

The above python declaration declares a function foo with positional-only arguments x1 and x2, positional or named arguments y1 and y2, and finally named-only arguments z1 and z2.

foo(1, 2, 3, 4, z1=5, z2=6) # x1=1, x2=2, y1=3, y2=4, z1=5, z2=6
foo(1, 2, x1=1, x2=4, z1=5, z2=6) # same thing
foo(1, 2, z2=6, z1=5, x2=4, x1=3) # valid -> named argument can be re-ordered
foo(x1=1, x2=2, x1=1, x2=4, z1=5, z2=6) # invalid -> x1 and x2 and positional-only
foo(1, 2, 3, 4, 5, 6) # invalid: z1 and z2 are named-only
  • This syntax is compatible with (a possible future addition of) optional arguments. It could use the same syntax than what struct would get. Example of that kind of possible syntax:
struct Struct1 {
    foo: usize = 1, // possible syntax to provide default with const initializer
    bar: usize = 2,
}
let s1 = Struct1 { foo: 1, bar: 2 };
let s1 = Struct1 { .. } // use default specified by default field
let s1 = Struct1 { foo: 1, .. }; // use the default for bar
let s1 = Struct1 { bar: 2, .. }; // use the default for foo

struct Struct2 {
    foo: usize, // no default
    bar: usize = 2,
}
let s2 = Struct2 { foo: 1, bar: 2 };
// let s2 = Struct2 { .. } // invalid, foo must be specified
let s2 = Struct2 { foo: 1, .. }; // use the default for bar
// let s2 = Struct2 { bar: 2, .. }; // invalid, foo must be specified

#[derive(Default)] // could also be a manual implementation of `Default`
struct Struct3 {
    foo: usize,
    bar: usize,
}
let s3 = Struct3 { foo: 1, bar: 2 };
let s3 = Struct3 { ..default() } // use default specified by default field
let s3 = Struct3 { foo: 1, ..default() }; // use the default for bar
let s3 = Struct3 { bar: 2, ..default() }; // use the default for foo

fn func { foo: usize = 1, bar: usize = 2 };
func(1, 2); // positional
func(1, ..); // use default for bar
func(..); default for both foo and bar
func{foo: 1, bar: 2}; // named
func{foo: 1, ..}; // default for bar
func{bar: 2, ..}; // default for foo
func{..}; // default for both foo + bar
// note: it could also be func(..default()) or any other syntax if more explicitness is needed
  • Open question: should argument re-ordering when calling a named-argument function be supported, just like reordering field when creating a structure is possible ?
struct Struct {
    foo: usize,
    bar: usize,
}
fn function {
    foo: usize,
    bar: usize,
}
let s = Struct { bar: 18, foo: 18 }; // valid
fn function{ bar: 18, foo: 18 }; // should this be valid?

For a language symmetry, it should be allowed, but I’m not sure it’s wise, since function(18, 21), looks a lot like function{bar: 18, foo: 21} even if the arguments where swapped.

However, If optional arguments are added, it may make some sense to allow argument re-ordering:

fn func{foo: usize = 1, bar: usize = 2, baz: usize = 3}; // hypothetical syntax with named + optional argument
func{bar: 22, baz: 33, ...} // hypothetical syntax where we expand all the default
func{baz: 33, bar: 22, ...} // should this be rejected if the above is accepted?
  • Open question: if optional arguments are added, should #[non_exhaustive] be supported to be able to add more arguments (assuming a default is provided) in a backward compatible way?
#[non_exhaustive]
fn func{foo: usize, bar: usize} -> String;

// calling code:
let f1 = func{foo: 1, bar: 2, ..};

// In a later version, `func` could become
#[non_exhaustive]
fn func{foo: usize, bar: usize, baz: usize = 3} -> String;
// which is semver compatible since the calling code doesn’t need to be modified

Could such function be used with a Fn trait, or converted to a function pointer?

  • Open question: if optional arguments + non exhaustive are added, could an argument with a default be "unstable", and thus not part of the stable API? (it’ just like non-stabilized function in std, than are hidden behind a flag)
#[non_exhaustive]
fn func{foo: usize, bar: usize, #[unstable] baz: usize = 3} -> String;

let s1 = func{foo: 1, bar: 1, ..}; // OK
let s2 = func{foo: 1, bar: 2, baz: 33, ..}; // KO: use of unstable argument
let s3 = func{foo: 1, bar: 2, #[unstable] baz: 33, ..}; // OK (note: syntax 100% bikesheddable)

// Note: a later version could become in a sem-ver compatible way
#[non_exhaustive]
fn func{foo: usize, bar: usize, #[unstable] foobar: usize = 3} -> String;
// or even
#[non_exhaustive]
fn func{foo: usize, bar: usize}

// In both case `s3` would be broken, but that’s ok since
// the user explicitly opt-in into a beta feature
2 Likes

As for the OP: I don't see the motivation in this, and I don't understand why we need "symmetry" between functions and structs and enums. They represent conceptually and practically different things – what benefit does it have to try and fit them into the same box? If you are looking for named arguments, I think there have been a number of proposals in the past that may be of interest to you.

On my phone right now so I'm unable to provide a complete and detailed description of it, but as a long-ish-time Swift user, my personal opinion is that Swift didn't manage to deal with the problem of named arguments very well. There have been decisions and trade-offs, and the overall result feels much more like "nothing ever works" rather than "it is so useful and convenient". It's almost exactly the aforementioned C++ ctor-vs-method syntax confusion: rules seem arbitrary at times, and heavy overloading of syntactic constructs can be annoying even for experienced users.

1 Like

Guess the real question is if named arguments could be kept but Swift pitfalls avoided..

One option I've not really seen considered is to support type-scoped macros:

Struct::new! { a: 456, b }

Upsides: Not really new syntax. Signals non-triviality with !. You can rename variables and handle ambiguities in the macro definition without making them visible in the function API. Would not have any meaningful implications for calling conventions or ABI, but it could maybe be useful to handle FFI calls under the hood.

Downsides: Perhaps too flexible. Harder to debug. Harder to write. Some might use :, others might use =, and other such idiosyncratic behaviors.

The benefit is that, conceptually, struct/enum-variant constructors are a kind of function -- they take values in as arguments/fields and return a new value -- this is reinforced by how Rust lets you treat a tuple struct/enum-variant constructor as a function, passing it to code taking impl Fn. If we unify struct/enum-variant constructors fully with functions, we automatically get the additional benefit of having per-argument stability annotations, defaults, #[nonexhaustive], functional update syntax (using S { ..x }), field init shorthand (S { x } instead of S { x: x }), and more -- structs have all those features if you include the various in-progress RFCs/proposals.

We get the additional benefit that we can easily teach people how to use the new syntax -- it's the same as struct constructor syntax. Pretty much all the other proposals either use new syntax which is harder to learn because it's different than any existing syntax, and/or they add extra unnecessary syntax (f(_ {a: 3})).

The language also becomes conceptually simpler because there doesn't need to be separate syntax for functions, structs, and enums, the call/construction syntax is the same across all callable types.

There's also the benefit that all named functions/type-constructors can always be called with the exact same syntax: name{ arg1: value, arg2: value, ..default } where arg names can be numbers.

3 Likes

There's benefit in Struct { 0: value, 1: value } not being a function, though, and that's that you know if you do it, then it's just moving values around and not calling arbitrary code. (This is important for e.g. unsafe and unwind safety.)

It's also worth noting that "tuple struct constructors" (i.e. Foo(0, 1)) aren't "like a function" or "can use as a function;" they're literally just a function defined by the complier.

If you declare struct Foo(u32, u32);, you get fn Foo(_0: u32, _1: u32) -> Foo { Foo { 0: _0, 1: _1 } } emitted by the compiler. It's literally just a function, nothing special. There is no special "tuple struct constructor;" it's just another function.

1 Like