As an example imagine a search function that returns the position of a character `c` in a string `str` that looks like that:
int search(String str, char c, bool case_sensitive, bool from_end)
I'd say that I would avoid this signature in general, because those two `bool`s make for an awkward call syntax: search("foo", 'f', true, false)
That's pretty opaque, even if you know what the two options are it's easy to forget the order. Now you might say "well see, that's where keyword parameters shine!" and you'd be right, it does help a lot: search("foo", 'f', case_sensitive: true, from_end: false)
But I'd argue that while this is good enough if you're coding in a dynamically typed language, it's actually sub-obtimal if you're coding in Rust for instance. Here you can do something even better: give these two options different types, this way not only it makes the code more explicit, it also means that it gets enforced by the compiler: enum SearchCase { Sensitive, Insensitive };
enum SearchDirection { FromStart, FromEnd };
search("foo", 'f', SearchCase::Sensitive, SearchDirection::FromStart);
Now that's not to say that I don't want or like keyword arguments, I just think that it's one of these situations where "when you have a hammer, everything looks like a nail". I sometimes miss keyword arguments in Rust, but it's the exception rather than the rule for me.For instance, even in the example you gave, it could be argued that the SearchCase type is really just a boolean which is being introduced to do the job of an argument label. If this enum is only used in this function call, that's going to add noise to the docs with fairly tenuous justification.
And this only works in the case of arguments which can be easily converted to enums. For instance if I have a signature which looks like this:
void search_buffer(int match, int start, int end, int stride)
It's going to be very difficult to look at the call site and infer what the meanings of these arguments are.I don't really agree with this, I think in general it's a good idea to type your parameters as tightly as it's reasonable to do. I don't think that introducing a one-time enum is particularly unreasonable. I don't really get the argument for reducing noise in the docs, typically nowadays API documentations are hyperlinked document, not a big PDF, you either look the symbol up or you don't.
An argument in favour of using types here would be in the situation where the "case_sensitive" parameter is not a literal in the code but something that's being passed as argument from an other function or stored in an object. The names could be slightly different, the conventions could change from one part of the code to an other etc... With dedicated types you can be reasonably certain that if the types match then it's probably safe to pass to the function, with human-level semantics that may require more caution.
Of course my example is so trivial that it could be argued to be over-engineered, but consider a function that takes angles as floating point numbers vs. a dedicated "Angle" type for instance, with the float you have to make sure that the underlying representation is correct (degrees? radians? turns?) whereas a dedicated type would typically have well-defined semantics.
>And this only works in the case of arguments which can be easily converted to enums. For instance if I have a signature which looks like this:
void search_buffer(int match, int start, int end, int stride)
I agree completely with you here. Keyword parameters definitely have their use cases. I just think that they should be used parsimoniously and, IMO, can be a code smell if used too often, especially in languages with a decent type system.Also, if Rust were to support named arguments (even better if with default values), you can use explicit types for your bools all the same. It’s not an either or situation.
If I understand it correctly, the equivalent builder pattern in Rust (which is essentially using methods to build a struct used to instantiate the object) would be:
let search = Search::new("foo","f")
.option_case_sensitive(true)
.option_from_end(false)
.build();
Where "new" returns a builder object/struct with passed in and default params, those options methods are optional and can change those defaults, and the build method actually builders the search object from the search builder struct with all the params.I'm not sure how well this style works with simple functions, unless there's a macro or crate that can do most of this work for you from a a spec.
1: https://doc.rust-lang.org/1.0.0/style/ownership/builders.htm...
Bool is also enforced by the compiler; adding ad-hoc types for things that have only two values is generally not something you should do without good reason. By the way, constructs like these are fairly common in Swift:
enum SearchType {
case literal
case regex
case extendedRegex
}
extension String {
func substring(matching needle: String, interpretedAs type: SearchType = .literal) -> Substring
}
which you'd use as something like "foobar".substring(matching: "o*ba", interpretedAs: .regex) // "ooba"But your point is well taken that, like many tools, you shouldn't abandon one set of good practices for another. There are certainly cases where a builder pattern or enum values are a better fit, which could be obscured as an option because keyword arguments "sorta kinda but don't really" address the same thing.
/// Doc comment doc comment doc comment
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum SearchCase { Sensitive, Insensitive }
now it starts to feel like heavyweight clutter.After all, even with keyword parameters there'll always be plenty of opportunity for one-off enums, if only for situations where more than two choices are possible. Take a typical "seek" function for instance, who can usually be performed relative to the current location, from the start or from the end.
search("foo", CaseSensitive, FromStart)
or options that take values: search("foo", WithCaseSensitivity(false))Maybe it's just me, but I would hate to imagine living in a world where my projects might be incomprehensible if I ever lost my .vimrc
For example if, today, I checked in code like performRequest(request, response, null, null, null) you probably wouldn't consider that good code, as it's not clear what the nulls mean there. Most people today would try to not write code like that. But if everyone was using an editor which displayed the parameter names, then it'd be fine. People would write that code, check it in, and so on. Which would mean that those people without the tool would have to confront such code.
- All arguments are keyword arguments. - Each function implicitly defines a struct type equivalent to its arguments.
I think if you combined this with some nice structural typing you can get a lot of the argument forwarding/builders and similar that people enjoy in dynamic languages without much/any overhead. Rough example:
fn add(a: int, b: int): int { a + b }
fn add3(c: int, rest: add::args): int { add(rest) + 3 }
A somewhat silly example but I think you can see the point. Of course you could also add language-level builder syntax to these structs and have something really nice.
I think the key here is that once you make the arguments a static type you can add loads of convenience like builders very easily.
https://github.com/idanarye/rust-typed-builder/blob/master/R...
The key is that it only generates code for types that actually get used. So if you always call the orders in the same order, it won't generate tons of code. That said, you could actually tweak the type-safe builder to enforce a certain order for calling the methods on the builder.
Also, it's not the order of arguments, it's the order of method calls :)
For all the attention given to static typing and unit testing for increasing developer confidence, I think named arguments belong up there as well. Makes reading code so much easier, especially the way they are implemented and encouraged in Swift.
As someone stuck doing PHP for my day job, they could buy an awful lot of goodwill from me by implementing something like this in PHP (well, that, and separating arrays from dictionaries, but that seems far less likely to happen).