We Have Named Arguments at Home

51 points by itamarst 7 hours ago on lobsters | 31 comments

giacomo_cavalieri | 7 hours ago

What a nicely written article

friction produces better APIs

Couldn’t agree more! I’m all for languages having a smaller set of features rather than being ever-growing

ancienthero | 5 hours ago

I used to be in the camp for keyword/optional arguments, but thinking about it more I've changed my mind completely. After seeing the implications of making parameter names part of the public API, I don't think the complexity of expressing that is worth it. Not to mention, the default values of optional arguments are also practically part of the public API, but that fact is hidden almost by definition.

I tend to agree that it's enough to semantically group related parameters into their own type. It makes writing callsites inconvenient, but isn't it nice to look at a function call and be able to intuit what it's doing, even if it's verbose?

It's true that people won't naturally do this, especially for functions with just a few parameters. But I think the reverse is true too, where keyword arguments passively encourage people to write god functions that force you to read the docs anyways.

yosefk | 6 hours ago

"All of the above might be useful, though localized, syntax improvements. But the hidden tax is that the language becomes more complex, for arguably little gain." -- you can't seriously say this about Rust, a very complex language; the extra complexity isn't much compared to what's already in there. The gain is IMO very real - you get a nice API without it becoming a design problem (which keeping the "complexity" out of the language makes this into) for half the functions you write (the post says this is actually an advantage, in practice instead of thoughtful workarounds creating generally useful types you will simply get bad APIs with ugly callsites most of the time)

ssokolow | 3 hours ago

you can't seriously say this about Rust, a very complex language; the extra complexity isn't much compared to what's already in there.

Keeping an eye on the complexity budget to stave off becoming the next C++ "just one more little thing" at a time has been deep in Rust's RFC process since the beginning. Hell, Rust RFCs were I learned the adjective "didactic" and the phrase "complexity budget" and the sentiment that Rust had already blown its complexity budget on ownership and borrowing and that all further additions must be viewed warily.

The gain is IMO very real - you get a nice API without it becoming a design problem

I disagree. A big part of the thesis of the post is that things like Ruby's redirect_to are design problems. ("These calls look like they’re invoking one conceptual operation, but they mean wildly different things.")

Another core element of Rust's design philosophy from the beginning has been "Code gets read much more than it gets written".

in practice instead of thoughtful workarounds creating generally useful types you will simply get bad APIs with ugly callsites most of the time.

I'm going to have to call [citation needed] on this one.

zaphar | 6 hours ago

I don't follow. Why do you think this friction will lead to bad APIs?

yshui | 3 hours ago

I wish Rust had Zig's inferred struct name. i.e. this:

let cropped = image::imageops::crop_imm(
    &img,
    Crop {
        x: 10,
        y: 20,
        width: 200,
        height: 100,
    },
);

becomes


let cropped = image::imageops::crop_imm(
    &img,
    _ {
        x: 10,
        y: 20,
        width: 200,
        height: 100,
    },
);

There is some chance that Rust will implement this maybe by the next edition

moltonel | 2 hours ago

That's not going to work with impl Trait arguments ?

kornel | 2 hours ago

Lots of things don't work with impl Trait arguments. arg as _, arg.into(), sometimes even &arg when you get &T instead of T's Deref or &dyn

gavinmorrow | an hour ago

I wish you could do this with enums. I really love how in Swift, you can do stuff like let color = .red. It would be nice to be able to do let color = _::Red; in Rust.

nytpu | 5 hours ago

I broadly agree with this (especially considering the conclusion that thinking about restructuring the API is better than poorly emulating these features in the first place) although it somewhat feels like making excuses for "why this design you don't like and can demonstrably be done more conveniently is good actually". But like a lot of annoyances in programming languages, Rust especially it seems, I fully understand why it's like that even if I abstractly wish it didn't have to be like that.

olliej | 5 hours ago

Back when I was first implementing restructuring assignment in JavaScript I realised that it made named arguments* really nice:

function f({x, y, z}) { … }

That said I’m not super keen on the struct based approach in strongly typed language as it means every function starts needing its own type specified. I don’t think the argument that the type belongs to your type(s) is particularly compelling: the type you’re defining is a property of the specific interface.

E.g let’s say you have two functions both of which reasonably have the same parameter names? Should they be duplicated or shared?

The arguments for types like size isn’t super strong - a size or a rectangle/bounds is a reasonable thing to have as a standalone type.

thisalex | 4 hours ago

It’s not about parameter names. It is about semantics. Are these functions operate on the same semantical entity? If yes — share!

olliej | 4 hours ago

Right - the examples in the article (things like Size, etc) are reasonable, but in many cases that's not what you have so you litter your API surface with myriad single use structs.

The more I think about it, the more I realize is that the problem is the labeled parameters are seen as a property the parameter, rather than as part of the function name.

e.g

fn foo(a:u32)
foo(a:1)
let foo_ref = foo; // I can't recall rust syntax

is considered a call to a function named foo. In languages like objective-c the "parameter name" is actually part of the function's name.

e.g. func foo(a:Int) foo(a:1); let foo_ref = foo(a:)

so it is essentially the standard operation_arg1_arg2(...) scheme C uses for "overloading", only the parameters in semantically relevant locations rather than disconnected from the parameter description.

kornel | 2 hours ago

In ObjC they are literally part of the function name.

let foo_ref = foo:a;

yshui | 3 hours ago

every function starts needing its own type specified

Vulkan API does that and it's kind of OK.

carlana | 6 hours ago

I tend to agree with this. Most of it applies to Go too, except for the part about default structs. It adds friction, but in a static language a little bit of friction can be good.

I do many/most of these things all the time. Is that not normal? Actually, when I first opened Steve’s article I expected it to read more like this one!

junot | 5 hours ago

My IDE that shows me parameter names at any call site. Whatever "friction" the author is depending on to enforce their idea of good taste isn't there with the right tooling. It would still be nice if Rust directly supported named parameters.

In both posts, it also seems assumed that if Rust gets named parameters then argument defaults and function overloading are inevitable features. I don't think that's correct.

I also doubt the compiler treats passing ownership of a struct to a function the same as passing the unwrapped arguments, so the struct-based workaround is not only added boilerplate to achieve a syntactic sugar that should be more directly supported, but it probably also has minor side-effects on code generation.

ssokolow | 3 hours ago

My IDE that shows me parameter names at any call site. Whatever "friction" the author is depending on to enforce their idea of good taste isn't there with the right tooling.

The argument, as I understood it, is "The approach Rust requires of you to manage large number of arguments pushes you to reconsider the shape of your API in ways that tend to surface new semantic elements to be factored out".

MatheusRich | 4 hours ago

spillybones | an hour ago

OP says instead of this,

request(url, follow_redirects: false);

all you have to do is this:

struct RequestOptions {
    timeout: Duration,
    follow_redirects: bool,
}

impl Default for RequestOptions {
    fn default() -> Self {
        Self {
            timeout: Duration::from_secs(30),
            follow_redirects: true,
        }
    }
}

request(
    url,
    RequestOptions {
        follow_redirects: false,
        ..Default::default()
    },
);

This is supposed to be good code?? I don't see how people can look at struct constructors and think that it's useful and convenient to have named field initializers and default values, but believe that actually it's bad to have those features for function arguments. Why should the programmer have to write 12 lines of horrendous boilerplate code and pollute their namespace and every call site with a FooOptions struct (or worse, use a builder pattern)? It's pure Sour Grapes to think that's actually better than named arguments.

isuffix | an hour ago

Yeah, I think the general point that friction when writing pushes you towards thinking more deeply should apply more to AI generated opinion pieces than to single function calls. I really don't need fifty paragraphs elucidating a twenty word prompt.

gavinmorrow | an hour ago

If you were to have default field values and elided struct names, you could instead have this. While it is still more verbose, it does become much more reasonable. (Even if you didn't have elided struct names, it wouldn't be too bad.)

You can also then pass around RequestOptions in other code as you like, for example if you have a bunch of calls with similar configuration.

struct RequestOptions {
    timeout: Duration = Duration::from_secs(30),
    follow_redirects: bool = true,
}

request(
    url,
    _ {
        follow_redirects: false,
        ..Default::default()
    },
);

schneems | 3 hours ago

An optional argument is, to some extent, an argument which may or may not exist. Rust has a type for that.

A thing I’m missing is to have a mix of required and optional fields in a struct. When you mark it Default ALL fields must provide a default and that also makes those types less useful (fewer guarantees). Or you end up having to wrap things in options as an extra bit of boiler plate on every invocation.

Maybe a PartialDefault trait that allowed for omitting some fields could be generally useful. It would err out if you tried to construct a struct missing a required field but otherwise fill in defaults for other ones.

ekuber | 3 hours ago

https://doc.rust-lang.org/unstable-book/language-features/default-field-values.html

I plan on stabilizing that before the end of the year. There's only one blocker around semantics and it is being actively worked on.

schneems | 2 hours ago

My man! Not 100% what I was thinking, but this is great.

For my use case I'm thinking something like this:

#[PartialDefault]
struct Contact {
    #[required]
    address: Address,
    #[optional]
    favorite_color: Option<String>
}

Then someone could:

Contact {
  address: &home,
  ..PartialDefault::default
}

But this would not be allowed to Contact { PartialDefault::default } (since address is a required field).

Ambroisie | 2 hours ago

I don't see the difference with the RFC feature. The fact that yours doesn't derive(Default)? I don't think that's required for default_field_values either (but the examples make it ambiguous).

schneems | an hour ago

Oh. Okay. Thanks for pointing that out. I guess I thought the Default derive was required.

melodyogonna | 2 hours ago

A lot of cope. Reads like the sort of article the Go community would write... Speaking as someone that loves Go. The point of optional arguments is that a predetermined default value is used if nothing is passed, passing none is not the same as passing nothing since none is a value in languages like Rust and Python.

Using structs for keyword arguments though is more palatable, even though it probably bundles the values together more than you'd like, and eliminates any method of capturing and forwarding the named arguments in variadic packs.

gavinmorrow | an hour ago

I had previously been in favor of named arguments, but I think this convinces me. Once you add keyword arguments, you have to add a whole host of other features that already exist, just for structs instead of functions.

The other benefit of using a struct when there are so many parameters is that you can now pass around that configuration in your own code. Say that you want to reuse your connection options in multiple places in your code, maybe even with one or two fields updated. Having that as a struct makes it trivial to do so, while arguments would force you to either make your own struct and destructure, have a ton of variables, or just duplicate. While with the struct, you can just do stuff like ConnectionOptions { timeout: 5, ..server_connection_options }.

I do think that one feature is kind of necessary for this, and another would be very helpful. I think that the default field values feature is needed so that you don't need to write a full impl Default. That would bring the verbosity, while still somewhat higher, much more in line with the named arguments version. I also think that being able to elide the struct name (e.g. _ { ... }) would help with verbosity, although imo it's not as generally needed as the more compact Default syntax.


(As as aside, I think it would be really cool if a language had every function accept "one" parameter, where that one parameter is a struct. [In this hypothetical language, structs would be able to have either named or positional fields; ie, tuple structs would exist.] There could be a bunch of syntax sugar to reduce verbosity, but I think it would be cool to be able to use everything that structs give you on arguments as well.)