Pre-RFC: Function parameter defaults

After lots of thinking over the years and reviewing prior discussions some time last year, I would like to propose two independent RFCs for function parameter defaults, and named function parameters. Due to time constraints I expect it will take me at least a couple more months to write the other one, but I feel like the first one is now ready enough for sharing as a pre-RFC:


  • Feature Name: function_param_defaults
  • Start Date: 2026-02-14

Summary

Add support for default values of individual parameters in function declarations.

impl TcpStream {
    pub fn set_nonblocking(
        &self,
        nonblocking: bool = true,
    ) -> io::Result<()> {
        self.0.set_nonblocking(nonblocking)
    }
}

// these two calls are equivalent
tcp_stream.set_nonblocking()
tcp_stream.set_nonblocking(true)

Motivation

In a lot of existing code, there are function parameters that exist to control some detail of that function's behavior. Commonly, these are Option<T>s, builder-like Options structs, enums, or bitflags-enabled flag structs. Many of these types have a default value, but it still has to be supplied explicitly, or there have to be multiple "versions" of the function for different amounts of arguments.

For a real-world example, these are the constructors of pulldown_cmark::Parser, at the time of writing:

impl<'input> Parser<'input, DefaultBrokenLinkCallback> {
    pub fn new(text: &'input str) -> Self {
        // ...
    }

    pub fn new_ext(text: &'input str, options: Options) -> Self {
        // ...
    }
}

impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
    pub fn new_with_broken_link_callback(
        text: &'input str,
        options: Options,
        broken_link_callback: Option<F>,
    ) -> Self {
        // ...
    }
}

With this feature, the first two could be combined to

impl<'input> Parser<'input, DefaultBrokenLinkCallback> {
    pub fn new(
        text: &'input str,
        options: Options = Options::empty(),
    ) -> Self {
        // ...
    }
}

(with one of the future possibilities regarding defaults on generic parameters, plus named parameters, the new_with_broken_link_callback use case could also be supported by new in a very nice way)

Guide-level explanation

A function's parameters may optionally have a default value:

fn foo(arg1: &str, arg2: u32 = 0) {}

This parameter may be omitted when the function is called, leading to the default being used:

foo("hello");
// is equivalent to
foo("hello", 0);

The default value must be an expression that can be evaluated in a const context.

// compiler error: `std::env::args()` is not a const expression
fn with_env_args(args: std::env::Args = std::env::args()) {}

Any parameters following the first with a default value must have default values themselves.

fn wrong_way_around(first: Option<String> = None, second: u32) {}

The arguments in a call to a function with parameter defaults are in-order, so the following is not possible:

fn with_defaults(a: bool = false, b: u8 = 255) { /* ... */ }

fn call_fn_with_defaults() {
    // compile error: expected bool, found u8
    with_defaults(0u8);
}

A function with one or more parameter defaults implements the Fn* traits multiple times. That is,

fn with_defaults(a: bool = false, b: u8 = 255) -> i16 { /* ... */ }

fn use_fn_no_params<T>(f: impl FnOnce() -> T) {
    // ...
}
fn use_fn_bool_to_i16(f: impl FnMut(bool) -> i16) {
    // ...
}
fn use_generic_two_params_to_i16<T, U>(f: impl Fn(T, U) -> i16) {
    // ...
}

fn pass_fn_with_defaults() {
    // all of these compile
    use_fn_no_params(with_defaults);
    use_fn_bool_to_i16(with_defaults);
    use_generic_two_params_to_i16(with_defaults);
}

Equally, with_defaults from the example above could be cast to any of fn() -> i16, fn(bool) -> i16 or fn(bool, u8) -> i16.

Defaults for parameters of generic type must be generic themselves that is the following is valid:

fn with_generic_default<T>(value: Option<T> = None) {}

… but this isn't:

// compile error: type mismatch, expected T, found bool
fn with_default<T>(value: T = false) {}

Reference-level explanation

A function's parameters may optionally be suffixed with = <default value> after the type. This is only supported for the Rust ABI, i.e. extern "C" fn may not specify a default value.

Function parameters inside traits can also have a default value. In this case, all impl blocks for the trait must repeat the same default value for clarity.

to be extended

Drawbacks

More language complexity.

Rationale and alternatives

to be written

Prior art

to be written

Unresolved questions

None right now.

SemVer considerations

Adding a default to an existing parameter outside a trait

… is a minor change because it can break type inference.

Adding a new parameter with a default outside a trait

… is also a minor change because it can break type inference.

Adding a default to an existing parameter inside a trait

… is a major (breaking) change because implementers must repeat the same default value in their function signature for clarity. This requirement could be relaxed in the future.

Adding a new parameter with a default inside a trait

… is a major (breaking) change because implementers must be updated accordingly.

Removing a default from a parameter

… is a major (breaking) change because callers that don't supply a value for it will be broken.

Changing a function parameter with default from concrete to generic

… is a major (breaking) change because it breaks calls that make use of the default (type inference will not be able to figure out the type). This is unlike the same change for a parameter without a default, which is declared as a minor change at the time of writing.

Future possibilities

  • Closures with parameter defaults
  • Named parameters (I intend to write a separate RFC for this)
  • Allow parameters of generic type to have a non-generic (or less generic) default, i.e.
    fn with_default<T>(value: impl IntoIterator<T> = Vec::new()) {}
    fn call_it() {
        with_default::<u8>();
        // equivalent to
        with_default(Vec::<u8>::new());
    }
    
  • Allow trait implementations to not repeat the default value from the trait definition (have the default be implied)
    • Would make it no longer a breaking change to add a default to a trait, unless the next point is also implemented
  • Allow trait implementations to have a default when the trait definition doesn't (as a type of refinement)

History

  • 2026-02-16: Added a paragraph + example to clarify that arguments must still be in-order when calling a function with multiple defaults
11 Likes

Can you give me an example? Because it seems to me that it instead allows the fn to accept more inferences. (Is that what you mean by "break type inference" or did you mean something else?)

It's a breaking change to add Fn* impls (they are fundamental traits), but there are probably ways around that.

  • Accept it anyway due to negligible breakage as function items aren't nameable
  • Consider each arity to be a different type

fn f(_: bool, _: i32 /* = 0 */) {
}

fn g(_: bool, _: i32 /* = 1 */) {
}

fn ex() {
    let _array = [f, g];
}

This compiles today, and you have [fn(bool, i32); 2]. Conceivably if both have defaults, it could be [fn(bool); 2] instead. So it's at least in some senses ambiguous. Alternatively, there could be some fallback preference for "no defaults"...

...but then consider if f adds a third defaulted arg _: f64, and at some later point, g does too. Now it infers to a new type, either changing semantics or breaking code. (If the fallback is "all defaults" you have the opposite problem.)

2 Likes

What I was thinking of was patterns like axum and bevy use to accept functions of various arities in higher-order methods. (e.g. routing::get for axum).

Adding a default to a parameter of a function that's passed to such a higher-order function w/o specifying type params would break compilation due to those type params no longer having one unambiguous substitution at that callsite.

I can post a concrete example later.

1 Like

To quote what I said last week elsewhere,

I continue to think that we should make using structs for this more ergonomic before considering named function argument proposals. Notably, that avoids all the questions like "and what do the fn traits look like?", plus are more convenient anyway for anyone wanting to wrap such a function, because forwarding "large groups of things" is annoying even with named parameters.


There have been loads of conversations like this before. I suggest you write the rationale and such first, not the "here's the solution I propose".

6 Likes

Well, this thread is about function parameter defaults, not named parameters. I posted it independently (a) because an RFC with both would be way too big IMO, and (b) because I think this makes sense as a feature on its own. Please let's not derail this thread with discussions about named parameters.

edit: Actually, how function param defaults could [not] work together with proposed improvements to "options structs" or whatever you want to call it, would be very much on-topic. But you seem to assert that function param defaults only make sense in combination with named parameters, and I think you should make a case to why, rather than just asserting it.

I know, and with more time I would have collected them all and summarized them (along filling out some more of the RFC template), and posted this as an actual RFC instead of just a Pre-RFC.

The text above has a Motivation section, so I don't understand this criticism.

1 Like

I believe that making defaults always come after the required arguments is a non-starter, because it means that making an argument default, or vice-versa, is a breaking API change.

1 Like

I don't think they're considerable in isolation, really. (And they're both mentioned in the OP.)

Function parameter defaults, at least assuming that more than one is allowed, to me inherently want named parameters to avoid the foo(1, None, None, None, None, None, 30) problem and be able to write foo(1, blah = 30) (or whatever) instead.

(Or, alternatively, they end up wanting overloading to be able to make different sets of possible arguments, but that has different -- worse IMHO -- problems.)

Specifically to me the Rationale & Alternatives section is the most important, because it's about why things work this way and why it's better than other possibilities.

The extant motivation section is basically just "I want function parameter defaults". It could similarly be the motivation section for "I want overloaded functions" -- where they're both just named new instead of having a default for the second parameter -- with almost zero changes.

3 Likes

I assume the compiler would insert the default argument value at the callsite? So at an ABI level there is no such thing as defaulted args (this is how it works in C++/Itanum for example).

In that case your proposed fn(bool) wouldn't work, especially not if the functions have different default args as in your example.

The other possible lowering would be to insert hidden Option<...> around the optional argument and have the callee resolve this. But that will likely be less efficient code gen..

(To design good and performant abstractions you need to consider the target machine code you want, rather than start with lofty high level goals.)

Your example of Parser::new could be written with named parameter defaults like fn new(text: &str, ☃tables: bool, ☃footnotes: bool, /* ... */) -> Self. But it also can be written without either feature as just fn new(text: &str, options: Options) -> Self if the Options argument is trivial enough to construct at the call site[1]. This could potentially look like:

Parser::new(text, .{..}); // get all the defaults
Parser::new(text, .{ gfm: true, .. }); // only enable GFM extensions
Parser::new(text, .{
    tables: true,
    footnotes: true,
    strikethrough: true,
    math: true,
    gfm: true,
    ..
); // enable the markdown extensions I commonly use

For a single optional argument, it's just not that much of a burden to write new(x, default())[2] if that gives the correct (and call-site self-evident) behavior. For something like Vec::new(), though, even Vec::new(16) is terribly unclear as to what that parameter means.

This is what Scott is getting at: are there use cases that are good API design at any default-parameter airity that don't additionally want optional parameters and would not be similarly satisfied by a syntax-light way to write Default::default()?

Yes, optional arguments and named arguments are orthogonal concerns and thus can be proposed and discussed independently. But like named arguments basically need to be optional to be good API design[3], the by-and-far majority usage of optional arguments in good API design is for said named arguments. Our[4] hypothesis is that the remaining cases of optional arguments would almost if not entirely be served just as well by a cheap way to say "use the default value." Simply omitting the argument is a very natural way to do so, but there are other solutions that would have a leser impact on API design principles, like .{..} which says "build a value of the contextually inferred type" (.{}) and "initialize all members with their default values," and also provides functionality useful for more than just defaulting the function argument suffix[5].

... I have now typed extensive context in the footnotes almost as long as the non-footnote post itself. There is a lot of context to this design space already that should be mentioned (conventionally in the "alternatives" section of the RFC template) any time the space is brought back to discussion, even for "pre-RFC" style "what about this approach" proposals. The semantics and behavior of optional arguments is by design fairly obvious, but there are actual design considerations to adopting them before other in-flight feature ideas that need to be resolved; it's not just a matter of someone needing to formally write up a proposal.


  1. Experience has shown that Default::default() is noisy enough to be distracting, and that requiring an added import is undesirable because it induces a context switch away from the code being edited. This call-site cost makes the API cost of a separate new_ext function reasonable. A new language feature's job is to tip that scale back in the favor of easier-to-consume API surfaces in the most generally applicable and least invasive way practical to do so. That's what preserves an actively evolving language's identity and "vibe" as opposed to building up legacy cruft that developers shouldn't use anymore but still need to understand for if they see it in an existing codebase. ↩︎

  2. On stable, the second argument needs to be spelled Default::default(), but use Default::default is something that everyone wants to make possible at some point, in a similar fashion to how use Enum::Variant works and transplants the generics on Enum onto Variant. ↩︎

  3. For clarity, I would consider Vec::with(capacity: 16) can be good API design, but it's only better than Vec::with_capacity(16) if the API conventions consistently use this naming scheme throughout, and Rust (by necessity) has an established naming scheme that doesn't. Plus, then you add the additional concern of Vec::with as a function item being unclear because argument labels aren't present, and Swift's experience with heavily argument-label-using language design has taught us that carrying said labels as part of the type is the wrong choice, they're part of the name. (Swift would now spell that function item as Vec::with(capacity:). ↩︎

  4. Scott's and mine only. I am not claiming to represent the opinion of any Rust team or working group one way or the other. ↩︎

  5. Type inference for .{} would likely to be known "at this point" and can't come from lexically later in the scope, the same way that's required for calling methods whose resolution potentially depends on an inferred type. The { .. } for defaults extends the already existing struct record update syntax Options { ..defaults }, only it takes the default values from the type implementation instead of a provided expression. Inferring the struct name in struct literal syntax would likely be limited in utility to function arguments due to the "type must be known at this point" requirement, but the syntax could also be extended to e.g. .all() which looks for a Self-producing function on the contextually known type. And { .. } is useful any time you want to build a struct with struct literal syntax with default values for some of the members, as we would likely allow providing default field values for some number of fields instead of only for the struct as a whole. ↩︎

6 Likes

I like this phrasing :+1:

Fits nicely, too, with imagining some kind of feature for "well in the same crate you can pass them exactly (to help remember to pass everything in wrapper, for example), but others have to pass the marker that there might be more later, so those additions are not source-semver-breaking".

But of course that gets back to "this is 3681 again, isn't it", and how this behaviour fits better in a struct than a function.

1 Like

Hmm... I just thought of a problem with defaults in trait methods. The default value can be exclusive to the trait definition site:

trait Trait {
    fn it(a: &str = include_str!("the_golden_gate_bridge.txt"));
}

As far as I know, the_golden_gate_bridge.txt needs to be present at build time because dependency crates are usually built from source. But the exact path it's in is non-trivial to figure out.

Its value may be expanded and statically displayed on docs.rs, but that's also a problem if the file is 1GB.[1]


// In crate aaa
trait Trait {
    fn it(a: &bool = cfg!(feature = "feature"));
}

This extracts the feature config of crate aaa, and cannot be copy-and-pasted to user crate bbb because it would refer to the feature config of bbb instead. Instead bbb will need to sync the default value with its Cargo.toml (or something else?).


Given the above, it seems like a good idea to delay default values in trait methods to another RFC?


Edit: There seems to be a solution to the latter: expose the default value

// In crate aaa
pub const AAA_DEFAULT = cfg!(feature = "feature");
trait Trait {
    fn it(a: &bool = AAA_DEFAULT);
}

  1. Has anyone ever needed to embed 1GB of data into their code like that? I'd be interested to hear your use case :grin:

    I just thought of this being a possible DoS vulnerability to docs.rs, but that seems false as the size of default values in generated HTML is equal to their size in the compiler's RAM (or AST representation at least?), so sandboxing the compiler is sufficient (not sufficient if the compiler optimizes to eliminate memory duplication of constants)

    ↩︎