Show photo of Nathan nixpulvis

Anonymous Struct Parameters: The Idea 7121 words published on October 19, 2026.

There have been a few recent posts on how Rust should adopt named arguments, and I even had some thoughts on the matter back in a comment I left on the issue in 2016. My original idea was never really something I expected to be adopted, but my point was always that named arguments are just structs, which is even more true now than back then given default field values (RFC 3681). It tried to show a direct correspondence between positional arguments with tuples and named arguments with structs, which was cute, but ultimately not good syntax.

But the point stands, Rust already has a named, reorderable, defaultable construct… the struct. I’m not the only one who thinks so. In the 2020 named arguments pre-RFC, CAD97 argued that making options structs more ergonomic was a better short-term goal than full named arguments, and the RFC it became was closed, with the lang team asking for a roadmap first. scottmcm has since pointed out that named arguments only need an inferred struct literal and a way to declare an anonymous struct in a function signature. This follows that direction, directly. RFC 3444 already proposes an inferred literal, but goes further than we need and also doesn’t cover anonymous structs in function signatures.

First we’ll show #![feature(anonymous_struct_parameters)]:

Which requires #![feature(struct_pattern_inference)] to then call:

These two features form the core of the struct argument proposal.

Then #![feature(anonymous_struct_pattern_elision)] gives:

And optionally, #![feature(parameter_type_elision)] adds corresponding elision of types that are described by their pattern:

Later we’ll show #![feature(named_arguments)] for a proposed syntax of what a full named argument sugar might look like:



1. Struct Args

Today in Rust you can pass struct arguments in a number of ways. Given the following struct:

struct Point {
    x: i32,
    y: i32,
}

You can use it in a function definition:

  1. As any other parameter: fn f(p: Point) {}
  2. With a pattern: fn f(Point { x, y }: Point) {}

The first form is just standard argument passing, which doesn’t destructure the named fields at all, while the second form destructures the named fields but names the struct type twice.

I’ll be introducing new syntax in stages, and noting all new syntax inside highlighted code blocks.

Anonymous structs in params

For a function argument without a named struct type to use, an anonymous struct type can be declared inline with struct { .. }. This is the base feature, #![feature(anonymous_struct_parameters)], and it’s an ordinary type in the pat: ty parameter position:

fn g(p: struct { x: i32, y: i32 }) {}

This only binds p, exactly as p: Point would. These new structs are anonymous, just like anonymous closure types, e.g. {closure@foo.rs:2:13: 2:21}.

Other than lacking a name though, an anonymous struct parameter is just a struct. The rules for their evaluation order, borrowing, patterns, and defaults (RFC 3681) are the ones Rust already has for structs, not new ones.

Anonymous struct types have come up on internals since 2016, usually as structural types, with { red: 25i8, .. } as both a type and an expression, and named arguments as one of the motivations. When it came up again in 2023, scottmcm replied with the point that named arguments don’t need structural types at all, just an inferred struct literal and “sugar to create an anonymous type in a function definition”, which is what’s proposed here. As recently as 2025, Josh Triplett separated three ideas that usually get conflated. The first is structural anonymous types, which seem unlikely to make progress soon, though we’ll explore some of their motivations in record types (in the upcoming follow-up post). The second is RFC 2102’s unnamed fields (tracking issue), which are about #[repr(C)] layout and not directly related to this. Their syntax does line up though, RFC 2102 also spells an anonymous struct type struct { .. }.

Struct pattern inference

Since the struct has no name, #![feature(struct_pattern_inference)] lets you bind its fields with _ { .. }, which is a struct pattern that infers its type. It works anywhere a pattern does, so the fields can also be renamed, borrowed, or skipped:

fn g(_ { x, y }: struct { x: i32, y: i32 }) {}
fn g(_ { x: horizontal, .. }: struct { x: i32, y: i32 = 0 }) {}
let _ { x, y } = point;

Calling a function with an anonymous struct argument is just like calling it with a named struct argument, except you can only use an _ to refer to it, the literal form of the same _ { .. }:

g(_ { x: 1, y: 2 });

But nothing is stopping you from inferring the type of a named struct argument either.

// recall: fn f(p: Point) {}
f(_ { x: 1, y: 2 });

Note that _ never fills in the type of a parameter. fn g(_ { x }) is an error for the same reason fn g(x: _) is. This is because function signatures don’t infer their types.

Josh’s third idea is passing a struct to a function without naming it. The f(_ { x: 1 }) form is the call half of what’s proposed here. This idea is materialized in the open RFC 3444, which proposes inferring a struct literal’s path (spelled .{ .. }). binrw’s maintainers point out that this plus field defaults (RFC 3681) would replace their args! macro and its inference hacks. However, RFC 3444 proposes a more general “path inference” and focuses on inferring the path of an enum, which we aren’t concerned with to satisfy ergonomic struct arguments.

I personally don’t accept that .{ } is better, since _ is already the convention for inferring types, and I have no issue with _::Variant naming an enum’s variant. Maybe this point will prove to be contentious, but I don’t see how .{ .. } really fixes it smoothly. This is something to consider now, since it might impact the broader design, but nothing we introduce here touches enum today. I’m not convinced inferred enum variants couldn’t be solved in a way that works naturally with _ { }.

With inferred literals, default field values (RFC 3681) carry over directly:

fn h(a: i32, _ { i, log }: struct { i: i32 = 1, log: bool = false }) {}
h(1, _ { .. });
h(2, _ { i: 2, .. });
h(3, _ { i: set_log(), log: get_log() });

So while this doesn’t give us default positional arguments, it does give us a way to define default fields for anonymous struct arguments.

Elided struct patterns

So far h has had to name i and log twice, once to bind them in the pattern and again to declare them in the type. Since we almost always want the fields themselves, we can leave the pattern off.

#![feature(anonymous_struct_pattern_elision)] lets an anonymous struct type act as its own pattern, binding each of its fields by name:

fn h(a: i32, struct { i: i32 = 1, log: bool = false }) {}

This is the form we’ll use for most of the rest of this post.

Fields are written just like parameters here, so they can use the same binding modes, mut, ref, and ref mut:

fn g(struct { mut x: i32, ref name: String }) {}

These only make sense when the type is also the pattern, so p: struct { mut x: i32 } is an error.

A field can also be an anonymous struct itself, though how an elided pattern should bind the nested fields is still an open question, which the upcoming follow-up post will go into.

Elided parameter types

Anonymous struct arguments don’t actually need elided parameter types, and neither does the named argument sugar later on, so this could just as well be its own proposal. But once a type can imply its pattern, it’s a natural time to let a pattern imply its type too.

This also removes the duplication of Point from the start of this section. With #![feature(parameter_type_elision)], a parameter’s type can be left out when the pattern already names it.

fn f(Point { x, y }: Point) {}

With an elided struct type, it could be written as:

fn f(Point { x, y }) {}

The rule is that a pattern which spells out a type can stand for both the pattern and that type. Struct and tuple struct patterns contain the type in their path names, a reference pattern has its referenced type, and tuples and arrays work if their members spell out their types.

fn f(p @ Point { x, .. })   // p @ Point { x, .. }: Point
fn f(Meters(m))             // Meters(m): Meters
fn f(&Point { x, y })       // &Point { x, y }: &Point
fn f((Point { x, y }, Meters(m)))  // (Point, Meters)
fn f([Meters(a), m])        // [Meters(a), m]: [Meters; 2]

This elision is purely syntactic since the type is basically just the pattern’s path copied over. Lifetimes can still be elided, e.g. &Point { .. } still works just like &Point does, and generic arguments are written in the pattern the way they would be in the type, e.g. fn f<T>(Pair<T> { a, b }) (or with a turbofish, Pair::<T>). A variant pattern like Shape::Square(s) names a variant, not a type, so it can’t elide anything.

API evolution and SemVer

Since an anonymous struct’s fields are named at every call, its field names are part of the function’s API, just like a public struct’s. That changes which edits to a function are breaking:

For named structs, #[non_exhaustive] makes adding fields non-breaking, but rather bluntly, since it forbids struct literals outside the defining crate entirely, and RFC 3681 doesn’t allow it on structs with default field values. So a library has to choose between defaults that callers can use with .., and adding fields without a breaking change. Anonymous structs have no struct item to put #[non_exhaustive] on, so they don’t even get that choice. If the two are ever allowed together, anonymous structs could opt in, or be non-exhaustive by default, meaning every literal from outside the crate has to write .., even one that lists every field. Then adding a field with a default would never be a breaking change.

2. Examples

Now let’s look at some realistic examples of this new syntax. Later we’ll see how to make it look even more like named arguments with some syntactic sugar. I have a hacked-together prototype implementation for every feature in this post, which the upcoming follow-up post will cover, but for now we’ll stick to the ones from Section 1 and leave named_arguments for later.

Unambiguous calls

Consider the std::fs::copy function, which has the following signature:

pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(
    from: P,
    to: Q,
) -> io::Result<u64>

Calling it can be ambiguous, since both arguments are just paths:

let a = Path::new("notes.txt");
let b = Path::new("notes.bak");
copy(a, b)?;
copy(b, a)?;  // oops, overwrites notes.txt

With an anonymous struct instead:

pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(struct {
    from: P,
    to: Q,
}) -> io::Result<u64>

Each call names its fields, so it’s clear which path is from and which is to:

let a = Path::new("notes.txt");
let b = Path::new("notes.bak");
copy(_ { from: a, to: b })?;
copy(_ { from: b, to: a })?;  // clearly wrong

Existing structs

_ { .. } isn’t tied to anonymous structs. It works wherever the expected type is a struct, so it works for every existing struct without changing anything:

struct Rgb { red: u8, green: u8, blue: u8 }

fn fill(shape: &mut Shape, color: Rgb) {
    unimplemented!()
}

fill(&mut shape, _ { red: 0xA8, green: 0x3C, blue: 0x09 });

let accent = Rgb { red: 0x1E, green: 0x90, blue: 0xFF };
fill(&mut shape, _ { red: 0xFF, ..accent });

// wgpu's `InstanceDescriptor`
Instance::new(_ {
    backends: Backends::VULKAN,
    ..InstanceDescriptor::new_without_display_handle()
});

Keep in mind that .. and ..Default::default() are different things. A plain .. takes each omitted field’s declared default (RFC 3681), which must be const, see non-const defaults, while ..Default::default() builds the struct’s whole Default value and takes the omitted fields from it. Neither is new here, they are just how structs already work.

Options structs

With default field values (RFC 3681), an options struct no longer needs the builder pattern, since .. fills in every field the caller leaves out:

pub struct CropOptions {
    pub width: u32,
    pub height: Option<u32> = None,
    pub x: u32 = 0,
    pub y: u32 = 0,
}

pub fn crop(img: &Image, options: CropOptions) -> Image {
    unimplemented!()
}

let img = Image::from_png("cat.png");
crop(&img, CropOptions { width: 200, .. });
crop(&img, CropOptions { width: 200, height: Some(200), .. });
crop(&img, CropOptions { y: 25, x: 15, width: 200, .. });

Today’s calls name CropOptions every time, and with _ { .. } they wouldn’t have to. But it’s still a whole separate type, with its own name and pub on every field, just to pass a few arguments to one function. An anonymous struct keeps the same fields and defaults, but puts them right in the signature:

pub fn crop(img: &Image, struct {
    width: u32,
    height: Option<u32> = None,
    x: u32 = 0,
    y: u32 = 0,
}) -> Image {
    unimplemented!()
}

let img = Image::from_png("cat.png");
crop(&img, _ { width: 200, .. });
crop(&img, _ { width: 200, height: Some(200), .. });
crop(&img, _ { y: 25, x: 15, width: 200, .. });

If the options do end up needing a name, switching back to CropOptions doesn’t change the calls, since _ { .. } still works to build a named struct.

Generics and lifetimes

Fields can use anything a parameter’s type can, including generic parameters, elided lifetimes, impl Trait, Self, and associated types:

fn map<T, U>(v: Vec<T>, struct {
    f: impl Fn(&T) -> U,
    limit: usize = 10,
}) -> Vec<U> {
    unimplemented!()
}

map(vec![1, 2, 3], _ { f: |n| n * 2, .. });
map(vec![1, 2, 3], _ { f: |n| n.to_string(), limit: 2 });

This works the way closures do. A closure written inside map gets a type that’s generic over map’s generic parameters, and so does the anonymous struct. Each instantiation of map, including the hidden type parameter that impl Fn introduces, gets its own instantiation of the struct. Nothing about the struct needs declaring, it just uses what the signature already has.

A lifetime in a field is the same as it would be in a positional parameter, whether it’s named like 'a here, or elided like &str would be in x: &str. Unlike a named struct, which would need struct PickArgs<'a>, the anonymous struct parameter never declares lifetimes itself:

fn pick<'a>(x: &'a str, struct {
    fallback: &'a str = "none",
}) -> &'a str {
    unimplemented!()
}

pick("", _ { .. });
pick("", _ { fallback: "empty" });

Traits

Trait methods can take anonymous struct parameters just like any other method. The trait owns the struct and its defaults, and each impl repeats the fields with the same types, just like positional parameters. Calls through generics and dyn work like any other method call:

trait Writer {
    fn write(&mut self, struct {
        data: &'static str,
        times: usize = 1,
    });
}

impl Writer for Buffer {
    fn write(&mut self, struct {
        times: usize,
        data: &'static str,
    }) {
        unimplemented!()
    }
}

buffer.write(_ { data: "hi", .. });
let w: &mut dyn Writer = &mut buffer;
w.write(_ { data: "hi", times: 2 });

3. Named Argument Sugar

Everything so far is struct syntax in new places. The proposed syntax sugar here is what looks like named arguments.

This is also the part I expect the most bikeshedding about. However, none of this is required since everything in Sections 1 and 2 works without it. Most of it is borrowed from other languages. The name: value arguments look like Swift’s argument labels, and like Swift, Python, and Julia, named arguments can have defaults. Separating positional and keyword parameters with a ; comes from Julia.

It does have costs though. Every call can be written two ways, and a ; signature obscures that the last parameter is a struct. Only the last argument can be written with names, and : in argument lists is taken for good. I think it’s worth it, but these are fair concerns.

Signature syntax: fn f(a: i32; x: i32)

A struct { .. } as the last parameter is noisy, especially in the common case where it holds the function’s options. Borrowing from Julia’s keyword parameters, a ; can end the positional parameters instead, and every parameter after it becomes a field of one anonymous struct, which is passed as the last argument. The ; has its drawbacks though, which ; vs * gets into, and there may be a better syntax here I just can’t think of.

With named argument sugar, crop and copy become:

fn crop(
    img: &Image;
    width: u32,
    height: Option<u32> = None,
    x: u32 = 0,
    y: u32 = 0,
) -> Image

fn copy<P: AsRef<Path>, Q: AsRef<Path>>(; from: P, to: Q) -> io::Result<u64>

Call syntax: f(a, x: 1)

Named arguments are the other half of the ;, though they fill a final struct argument regardless of how it’s declared. They are just sugar for a _ { .. } literal as the last argument. Once an argument is written name: value, it and every argument after it form the struct. The calls from Section 2 become:

copy(from: a, to: b)?;

crop(&img, width: 200, ..);
crop(&img, width: 200, x: 15, y: 25, ..);

fill(&mut shape, red: 0xA8, green: 0x3C, blue: 0x09);

The .. at the end of crop’s calls fills in the defaults. There are some more details and questions about Defaults below.

Positional parameters never become labels in this proposal. A parameter is a pattern that may not have a name, like (x, y): (i32, i32) or _: u8, and turning its name into API would make every rename a breaking change. So foo(a: 1) with fn foo(a: i32) is intentionally an error. The argument is either passed by position, or the parameter moves into a struct.

The syntax happens to be free, since expr: Type in expressions (type ascription) was removed, and macro_rules! can’t write $e:expr :. It does claim : in argument lists. f(x: 1) always means “the field x of the final parameter struct of f”.

; vs *

Python’s bare * for keyword-only parameters, as in def f(a, *, b=1), is similar. Unlike Julia’s and Python’s though, the ; doesn’t make a second kind of parameter, it’s just another way to write a struct.

Python’s * does have one real advantage though… It’s its own item in the list, so in a long signature it’s hard to miss, and adding or removing a parameter on either side of it in a multi-line parameter list only touches that parameter’s line. The ; takes the place of a comma instead, so in a signature that doesn’t fit on one line, like crop above, it ends the last positional parameter’s line. A short one stays on one line, fn f(a: i32; x: i32).

I personally like ; when used in a single line, but named arguments are likely to be numerous, and split across lines. * works in languages like Python and Ruby, which don’t have the strong association of * with deref, but in Rust I don’t like it. I’m not sure what’s better however.

Defaults: f(a, x: 1, ..)

Named arguments end with .. to fill in the defaults of any fields left out, the same as a struct literal, or with ..base to take them from another value. Without the .., every field has to be given:

crop(&img, width: 200, ..);
crop(&img, width: 200, height: None, x: 0, y: 0);

fill(&mut shape, red: 0xFF, ..accent);

The .. is required, since it’s what shows which fields were defaulted. Leaving it off, so that named arguments imply it, would be closer to how other languages do it, at the cost of visibility, which Rust tends to prefer.

A .. on its own isn’t a named argument though. f(..) and f(..base) are already a RangeFull and a RangeTo, as in v.drain(..). We could pick the meaning by the type of the parameter it fills, so f(..) would mean f(_ { .. }) when that parameter is a struct, and stay a range otherwise. Passing a range where a struct is expected is an error today, so no existing call would change meaning, but .. could no longer be read without knowing the signature. This is a somewhat unfortunate existing conflation in Rust’s syntax.

My upcoming follow-up post will go further into implying .. instead, whether named structs should get it too, and leaving out a last struct whose fields all have defaults.

4. Relating to Recent Posts

The recent posts from the intro each take a different position on named arguments in Rust. Endler argues structs already do the job, Botahamec wants pub parameters with defaults, and Klabnik is open to labels but not defaults. Each is worth answering on its own terms, so here’s an overview of how this design compares to them, with more details on each below.

  This design Structs today (Endler) pub parameters (Botahamec) Names only (Klabnik)
Named parameters Fields of a struct { .. } parameter, or parameters after a ; Fields of a named struct Parameters marked pub Labels on existing parameters
Reordering Yes Yes Yes Not settled
Defaults .. with RFC 3681 field defaults, or ..Default::default() for named structs. Still undecided: implying the .., and optional fields, which I’m skeptical of ..Default::default() May be non-const, omission implicit No
Evaluation order As written As written — Raised as a problem
Patterns as parameters Struct pattern — pub name @ pat Raised as a problem
fn pointers Names and defaults kept, distinct per function (sharing when the fields match is an open question) Names kept in the struct type Names erased Raised as a problem
Adding a parameter Non-breaking if defaulted, for callers that wrote .. Non-breaking if defaulted, for callers that wrote ..Default::default() Non-breaking if defaulted —
Per-function boilerplate None A named struct per argument bundle None None
New syntax struct { .. } as a parameter type, _ { .. } as an expression and a pattern, elided struct patterns and parameter types, and the optional sugar for name: value arguments and ; parameters None pub parameters, name: value arguments, defaults name: value arguments

Klabnik’s post is a position post and not a proposal, so its column records the problems it raises rather than answers. Macro crates like bon and structx fill the same gap today, with a builder or a macro at each call.

Botahamec: names, defaults, and pub parameters

In “Named and Optional Arguments are Awesome”, Botahamec tours Dart, C#, and TypeScript, then looks at what Rust does without them. HashMap has eight constructors to cover three optional parameters. print(text, bold, italics, underline) is easy to call with the flags swapped. The options-struct workaround needs a type, a Default impl, and ..Default::default() at every call, and it falls apart when only some fields have defaults.

Their proposal resembles a 2016 pre-RFC. A parameter marked pub can be passed by name, and parameters may have defaults, which callers can omit. A pub parameter can still be passed by position too.

pub is a poor fit for this, since it has nothing to do with visibility (a private fn can have pub parameters, and pub(crate) x: u32 has no sensible meaning), and the post itself flags the keyword as an open question. But that’s a small point about a single keyword.

// Botahamec's proposal
fn print_labeled_measurement(pub value: i32, pub unit_label: char = 'm') { .. }

print_labeled_measurement(5);
print_labeled_measurement(5, 'h');
print_labeled_measurement(value: 5, unit_label: 'h');
print_labeled_measurement(unit_label: 'h', value: 5);

With an anonymous struct, the same function’s named parameters become fields, which are only ever passed by name.

// anonymous struct
fn print_labeled_measurement(struct { value: i32, unit_label: char = 'm' }) { .. }

print_labeled_measurement(_ { value: 5, .. });
print_labeled_measurement(_ { value: 5, unit_label: 'h' });
print_labeled_measurement(_ { unit_label: 'h', value: 5 });

// with the named argument sugar
print_labeled_measurement(value: 5, ..);
print_labeled_measurement(unit_label: 'h', value: 5);

There’s no equivalent of print_labeled_measurement(5, 'h'), which is intentional. Like C#, Botahamec’s proposal lets a named parameter still be passed by position. With anonymous structs, a parameter is either positional or a field, and a field can only be passed by name, whether that’s in a _ { .. } literal or with the named argument sugar.

That’s why existing functions can’t be backported, which Botahamec counts against struct name inference. A function’s positional parameters stay positional, and taking names means moving them into a struct, which is a breaking change like adding any other parameter.

Botahamec also forbids defaults on trait methods, since it’s unclear whether the trait or the impl should own them, and only the trait’s would work through dyn. Here the trait owns them, and calls through generics and dyn use them like any other (Section 2).

Their HashMap example replaces the eight constructors with one function, quoted here as written. It doesn’t address generic parameters yet. S and A are never declared, and a default like A = Global only typechecks when A is Global, so the post sets the problem aside for its “Future possibilities” section.

// Botahamec's proposal, generic parameters ignored
pub fn new(
    pub capacity: usize = 0,
    pub alloc: A = Global,
    pub hasher: S = RandomState,
);

An anonymous struct has to address that problem now. It can still replace the eight constructors with one function, though the hasher and the allocator become Options that the body fills in:

// anonymous struct
fn new_map<K, V, S: BuildHasher + Default, A: Allocator + Default>(struct {
    capacity: usize = 0,
    hasher: Option<S> = None,
    alloc: Option<A> = None,
}) -> HashMap<K, V, S, A> {
    HashMap::with_capacity_and_hasher_in(
        capacity,
        hasher.unwrap_or_default(),
        alloc.unwrap_or_default(),
    )
}

let m: HashMap<u8, u8> = new_map(_ { capacity: 16, .. });

The bigger difference is how a parameter opts in. Botahamec’s proposal needs a keyword for it and anonymous structs don’t. A parameter opts in by being a field, and visibility isn’t overloaded to mean a parameter is a named argument.

Klabnik: open-minded concerns

In “Arguing about arguments”, Steve Klabnik is open to labels on existing parameters, foo(x: 5, y: 6) for fn foo(x: i32, y: i32), but not to optional or default arguments, because they hide what a call passes. They also list what makes named arguments hard in Rust specifically. Parameters are patterns and may not have a name, fn pointers erase names, trait impls can rename parameters, reordering arguments raises the question of evaluation order, and every parameter name quietly becomes part of the API.

Klabnik’s objection to defaults is fair, and the answer is that making names explicit is what lets defaults be safe. Fields are named only because the function put them in a struct, and they can only be passed by name, so there’s no positional form to swap and no argument left out of the middle of a list, f(a, c) for f(a, b = 0, c). Defaults are owned by the function (by the trait, for a trait method), and a call that relies on them says so with .., with or without the sugar. Adding a parameter is also safe, since a new field with a default doesn’t break callers that wrote ...

Where Botahamec opts parameters in with pub, Klabnik doesn’t opt them in at all, and that’s the bigger difference. If every parameter can be labeled, the function’s author can no longer decide how an argument is passed. Labels would have to be optional for existing calls to keep compiling, so copy(a, b) still compiles, and there’s no way to require names for arguments that are confusing by position, like from and to. That’s the whole point of naming them. With anonymous structs the author decides. Positional parameters are passed by position, fields are passed by name, and the signature shows where one ends and the other begins, so copy(_ { from: a, to: b }), or copy(from: a, to: b) with the sugar, is the only way to call copy.

The other problems are secondary. Every parameter name in every published crate would become API after the fact, including names chosen only for the function body, like buf, _x, hygienic macro names, and bindgen’s arg1, so renaming any of them would break someone. Each parameter would also get two APIs, since reordering breaks positional callers and renaming breaks labeled ones, and mixed calls would need new rules for which slot a positional argument fills after a label.

The rest of Klabnik’s list already has answers, because each has an existing struct rule. None of these are new to Rust. Here’s how anonymous structs handle each one.

Evaluation order

Fields are evaluated in the order written at the call site, as in any struct literal. So this compiles:

fn consume(struct { data: Vec<u8>, length: usize }) { .. }

consume(_ { length: data.len(), data: data });

and swapping the two is a use-after-move error, exactly as it would be for a named struct:

consume(_ { data: data, length: data.len() });
// error[E0382]: borrow of moved value: `data`

Patterns as parameters

Klabnik points out that a Rust parameter is a pattern, not a name. fn foo((x, y): (i32, i32)) introduces two bindings, so there’s no single name to label the argument with, short of inventing separate external and internal names.

Anonymous structs don’t need either. Only fields are passed by name, and a field always has one, which is its label at the call. With an elided pattern it’s also the binding in the body, and when the body wants a different name, an explicit _ { .. } pattern renames it like any struct pattern. Positional parameters keep their patterns, since they’re never passed by name.

fn foo((x, y): (i32, i32), struct { scale: i32 = 1 }) -> i32 { .. }
fn bar((x, y): (i32, i32), _ { scale: s }: struct { scale: i32 = 1 }) -> i32 { .. }

foo((1, 2), _ { scale: 2 });
foo((1, 2), _ { .. });

Defaults

Defaults are RFC 3681 defaults. They’re declared on the field, and used when the caller writes ... They can use the function’s type and const parameters (fn foo<const A: usize>(struct { bar: usize = A })), and associated consts through their bounds (fn foo<T: HasN>(struct { n: usize = T::N })).

Renaming hazards

A field’s name is part of the function’s API, the same as a public struct field (Section 1). Positional parameters are unaffected, so a function opts in, one parameter at a time, by moving it into the struct. Nothing becomes API by accident, and a typo like widht gets the same error, with the same suggestion of width, as it would in any struct literal.

Trait impls

An impl can’t rename fields. It repeats the trait’s fields by name, in any order, and a renamed field is an error (Section 2). It has to be that way, since callers through generics and dyn only see the trait, so the trait’s field names are the ones they write.

fn pointers

A function used as a value keeps its anonymous structs as parameter types, so names survive through the pointer, and so do defaults:

fn foo(struct { a: i32, b: i32 = 10 }) -> i32 { .. }

let g: fn(_) -> i32 = foo;
g(_ { b: 2, a: 4 });
g(_ { a: 5, .. });

Two functions with the same fields don’t share a pointer type (Section 5), though the upcoming follow-up post will sketch one way they might.

Endler: structs already do this

In “We Have Named Arguments at Home”, Matthias Endler argues the job is already done. A struct gives you names, any order, typo checking, per-field docs, and defaults, and it’s a real type you can store, validate, and pass along. Names belong to a type, not to the calling convention.

That’s the same premise anonymous structs start from, that named arguments are just structs. Anonymous structs are convenience on top of Endler’s model, not a replacement for it. The function still takes a struct, with all of those properties, and the anonymous struct and the inferred literal only remove the ceremony of declaring it and naming it at every call. It’s the have-your-cake-and-eat-it-too approach, with the semantics of structs and without the boilerplate.

Endler’s crop_imm example, borrowed from Klabnik, declares a struct just for this one function:

// Endler
struct Crop {
    x: u32,
    y: u32,
    width: u32,
    height: u32,
}

fn crop_imm<I: GenericImageView>(
    image: &I,
    crop: Crop,
) -> SubImage<&I> { .. }

crop_imm(&img, Crop { x: 10, y: 20, width: 200, height: 100 });

That’s the case anonymous structs were made for:

// anonymous struct
fn crop_imm<I: GenericImageView>(image: &I, struct {
    x: u32,
    y: u32,
    width: u32,
    height: u32,
}) -> SubImage<&I> { .. }

crop_imm(&img, _ { x: 10, y: 20, width: 200, height: 100 });

When the arguments need to be a real type, for invariants, for storing, or for passing along, declare one and use the elided type from Section 1, as in fn crop_imm<I: GenericImageView>(image: &I, Crop { x, y, width, height }). Callers don’t change, since _ { .. } builds either one. The call Endler’s post imagines a pile of language features for gets close without any sugar. It basically just needs _ { .. } around the options and real syntax for the duration and headers:

fn request(url: impl Into<Url>, struct {
    timeout: Option<Duration> = None,
    redirects: bool = true,
    headers: Vec<Header> = Vec::new(),
}) -> Result<Response, Error> { .. }

request("/hello", _ {
    timeout: Some(Duration::from_secs(5)),
    redirects: false,
    headers: vec![
        Header("Accept", "application/json"),
        Header("X-Foo", "bar"),
    ],
})?;
request("/hello", _ { .. })?;

With the named argument sugar from Section 3, the _ { .. } goes away too, and the call has the shape Endler imagines.

Endler also credits field-init shorthand for removing the foo=foo repetition Klabnik points out. _ { timeout, redirects } keeps it, though the named argument sugar has no shorthand of its own.

Where they agree

Side by side, the posts agree on more than it might seem at first. Everyone is trying to make names at the call site work. Botahamec’s objection to the options struct isn’t its meaning but its ceremony. In their words, “I don’t see much reason to name a new struct if it’s only going to be used for the one function.” Endler’s structs already answer most of Klabnik’s hard problems, because struct literals already have rules for names, order, and evaluation.

That leaves one cost, declaring and naming a struct for every function. It’s why the pattern shows up in public APIs and almost never in private code. Anonymous structs take Endler’s model and remove that cost, which is mostly what Botahamec is asking for. Klabnik’s objection to defaults is the one real potential disagreement, and making the named parameters explicit helps, I think.

That’s why I think structs are the right direction, rather than a separate named argument feature. Everything here is built from parts Rust already has, struct literals, struct patterns, and RFC 3681 defaults, with the struct’s name inferred and its declaration moved into the signature. Each post gets what it asks for without giving up what the others care about. Endler’s structs stay real types, Botahamec loses the boilerplate, and Klabnik can see the defaults at the call, since struct literals and named arguments mark them with ... Names, order, evaluation, and API stability all follow rules Rust users already know from struct literals, instead of a second set of rules for arguments. And it doesn’t close any doors, since the sugar from Section 3 is optional and can wait until the base feature is accepted and tested. I also think it will gracefully continue into structural records, if/when we ever want to tackle that.

5. Limitations and Open Questions

These will be covered in more detail, along with the prototype’s own gaps, in the upcoming follow-up post.

Non-const defaults

Field defaults are RFC 3681 defaults and must be const. A default can’t call RandomState::new(), or S::default() through a bound, so a field like that becomes an Option the body fills in. This is a limit of RFC 3681, not of anonymous structs, and its tracking issue already lists non-const values as an open question.

Function types

A function used as a value keeps its anonymous struct as a parameter type, so two functions whose structs have the same fields still have different types, and if c { returns_anon_a } else { returns_anon_b } doesn’t compile.

Macros

Most procedural macros parse with syn, which expects every parameter to be pat: ty. The core features keep that shape, so syn just needs the new syntax, as it got = default fields in 3.0. The elision features and named_arguments change the shape, which means macros that read each parameter, like #[tracing::instrument], need updating too.

macro_rules! is subtler. _ { .. } changes what pat, expr, and stmt fragments match, so existing macros can pick a different arm, even in crates that don’t enable struct_pattern_inference. That makes it the only part of the proposal that needs an edition. The upcoming follow-up will have the details.

Tooling

rust-analyzer and tree-sitter-rust have their own parsers and need updating separately. rustfmt uses rustc’s parser, so it mostly works already.

6. What’s Next

fn send_feedback(struct {
    from: impl TryInto<User, Error = InvalidEmail>,
    to: impl TryInto<User, Error = InvalidEmail>,
    message: &str,
    mood: Mood,
}) -> Result<(), InvalidEmail>;

send_feedback(_ {
    from: "you@example.com",
    to: "nathan@nixpulvis.com",
    message: "
      Named arguments are dumb, Rust shouldn't even allow
      multiple arguments. Everything should be curried:
      
      λ(x, y).e := λx.λy.e
    ",
    mood: Mood::Angry,
})?;

Seriously though, I’d like to hear what you think, where this breaks, what APIs it doesn’t fit, or rules that surprise you, over on the internals.rust-lang.org thread.