State of named function parameters in Rust?

I just reached Item 29 in Item 29: Listen to Clippy - Effective Rust

They have the following nice example:

pub fn circle_area(radius: f64) -> f64 {
    let pi = 3.14;
    pi * radius * radius
}

So what will happen, when user code calls that function with a diameter argument instead of the intended radius? A careful code audit might catch this error, but I think it still can happen and might remain hidden until our moon lander crashed. Using unit or integration tests would not really help catching that user code error. Well, using a circle struct instead of the float parameter would help. But when we use plain parameters, a named call like circle_area(radius = 2.0) or sin(degree_angle=45.0) might help?

[EDIT]

Well, I assume using Enum parameters might help?

Named parameters wouldn't prevent the caller from passing a diameter either. If you truly want to prevent this from happening, then you must use newtypes for them (e.g. a type named Diameter and another one named Radius).

...or more descriptive or explicit function names:

pub fn circle_area_from_radius(radius: f64) -> f64 { ... }

There's no consensus about this. It can be done in multiple ways, e.g. making a syntax sugar for passing a struct with fields, or copying ObjC's trick of making argument names part of the function name, or be a more elaborate dedicated functionality that looks like function overloading.

There's no consensus whether it's needed, because there are existing workarounds (there's builder pattern, you can make multiple functions, or accept boilerplate of passing a struct with ..Default).

Additionally, some developers doubt whether it's a good idea at all due to bad experiences with accidental complexity it may cause (C++) or overuse for making too clever functions that do too many things at once (Python).

There's a question what to do with the standard library if optional arguments became the norm. The standard library is stable forever and can't be changed, so it could look weird and dated if everything except it used optional parameters.

Names parameters are brought up from time to time on internals and on the rust-lang zulip, so there seems to be recurring will to have them.

Just the other day I ended up creating a gen_test(ch: u8, seq: u64, prio: i16) method, but calling it (Msg::gen_test(0, 1, 0)) is deeply unfunny (at least until I had finally settled on which order the parameters should be in). I would definitely welcome named parameters for these kinds of situations.

Another thing is this:

init_runtime(true, true, false);

The generic tip is "Use enums rather than bools", and I definitely agree in most cases. But in some cases the code ends up having way too many "this-is-a-bool" enums. I really would like the option to use named parameters instead.

With that said, there's this "workaround":

struct TestParams {
  ch: u8,
  seq: u64,
  prio: i16
}

fn gen_test(TestParams { ch, seq, prio }: TestParams) {
  // ...
}

fn foo() {
  gen_test(TestParams { ch: 0, seq: 1, prio: 0 });
}

I used to dislike this syntax, but as with most things in life -- I eventually learned to live with it, and nowadays I don't mind it. (Haven't reached "I finally realized why this syntax is genius, and now I love it!" yet, but if history has taught me anything, I may end up there in the end).

An alternative, without any judgement:

struct TestParams {
    channel: u8,
    sequence: u64,
    priority: i16,
}

impl TestParams {
    fn generate_test(&self) {
        // ...
    }
}

fn foo() {
    TestParams {
        channel: 0,
        sequence: 1,
        priority: 0,
    }
    .generate_test();
}

(In general, my preference for long names grows year by year...)

I would go as far as to say it's idiomatic to use long names in the Rust ecosystem.

I'll get there eventually, but I'm currently still enforcing a 80 column limit and I find that 4 space indentation and long names cause too many unnecessary wraps. I know it's a me-problem, and that I need to wake up and smell the non-80-column consoles, but I'm not quite there yet.

another alternative:

struct Channel(u8);
struct Sequence(u64);
struct Priority(i16);

fn generate_test(channel: Channel, sequence: Sequence, priority: Priority) {
    todo!();
}

fn foo() {
    generate_test(Channel(0), Sequence(1), Priority(0));
}

Are you taking about Swift? My opinion is : it's too complicated and still doesn't eliminate human mistakes.

When have C++ got named parameters? Do you mean designated initializers? These can work in Rust, too, and in almost the same way.

Except we have already seen it changed in a edition. If really needed that can be used for named parameters, too.

Desire, perhaps, but not necessarily will.

I would encourage anyone suffering from James Shore: Primitive Obsession like this to start making types instead. Pass Priority::MAX or Priority(20) or something.

Ah, a case of Boolean Blindness | Existential Type

rust-analyzer shows parameter names in VSCode. I think this (and other IDE features) mostly scratches the itch for named parameters, and when it's confusing enough, I make a struct of parameters (which clippy encourages once you have enough). I personally do not care for using a newtype for arguments (e.g. Radius(1.0)).

I think builders is the closest I've seen to consensus for popular crates that have a lot of properties, but they are a lot of work and noise on both the implementation side and the caller side.

I'd expect any language native approach to end up trying to optimize that, eg being able to have syntax like:

create_window(_ {
    title: "Hello, World".into(),
    fullscreen: true,
    ..
})

as sugar for explicitly providing the WindowOptions type and a call to default, but it's low value for the complexity it raises, so I don't think there's much enthusiasm.

Well default_field_values - The Rust Unstable Book even already exists on nightly :slight_smile:

Handy. Though I find the distinction between { .. } and default() odd it is a useful one!

I think the concerns I remembered were more about the implicit type name, though I'm not sure what the issues were specifically.

There's lots of thoughts in https://github.com/rust-lang/rfcs/pull/3444, as well as in a bazillion IRLO threads.

It's hard, because in some places it's obviously good and in some places it's obviously bad.

I've meant overloading, which can be used to add extra optional args (Rust proposals typically make the named args optional, hence the overlap).

I guess

let diameter = 3.142342927;
let area = circle_area(radius = diameter);

would cause some raised eyebrows.

For sure. I didn't say having named parameters wouldn't help, what I said is that it doesn't prevent the problem from happening.

Welcome to 1999 and Hungarian Notation :upside_down_face: