We Have Named Arguments at Home
45 points by itamarst
45 points by itamarst
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
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.
"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)
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.
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.
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.
It’s not about parameter names. It is about semantics. Are these functions operate on the same semantical entity? If yes — share!
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.
every function starts needing its own type specified
Vulkan API does that and it's kind of OK.
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!
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.
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".
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,
},
);
I've written something similar years ago https://thoughtbot.com/blog/rust-doesn-t-have-named-arguments-so-what
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.
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.
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).
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).