5 ms·
I like named/keyword arguments too but I think one needs to be careful not to overdo it, especially in statically typed languages. I think it's sort of a last r
by simias 6y ago
I like named/keyword arguments too but I think one needs to be careful not to overdo it, especially in statically typed languages. I think it's sort of a last resort when the call syntax can't be disambiguated by other means.
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.
- oefrha 6y agoYou’re forgetting another important use case of keyword/named arguments: default values. In Rust either every call site needs to specify the default values, or you use a None for the default which is not at all readable. 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.
- kbenson 6y agoWouldn't a struct for named options that have defaults which is passed as the last option emulate optional named arguments with default values?
- skohan 6y agoOf course in the case of Rust this would mean potentially one type per function being added to your implementation. That’s a lot of boilerplate for a problem which is solved really well by defaultable named parameters
- kbenson 6y agoI agree it seems like a lot of boilerplate, but from what little I've played around with Rust, it seems they've already adopted strategies similar to this for instantiating objects with the builder pattern[1], but the nuts and bolts are abstracted away so the interface is slightly different. 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.html https://doc.rust-lang.org/1.0.0/style/ownership/builders.htm...
- kiawe_fire 6y agoI still think there are wider use cases for named arguments. For example, even disambiguating between which argument is the "string" and which is the "substring to be found", or the classic "x, y, width, height" list in graphics programming. 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.
- skohan 6y agoI would question that your rust example is better than the named argument example. I think it's situational at best, and a workaround at worst. 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.
- simias 6y ago>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. 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.
- yoz-y 6y agoThat's why Swift does support unnamed parameters as well, and they can interweave with named ones. If the type of the argument is explicit enough, you can make the parameter name at call site optional.
- wool_gather 6y agoNitpick (I think this is just a miswording rather than a misunderstanding): it's not optional. You can have unlabelled args, but that's enforced too: the label must be included or omitted at the call site according to the function declaration.
- yoz-y 6y agoTBH I was usure and kind of lazy to check. Thanks for pointing it out!
- saagarjha 6y ago> 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 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"
- barrkel 6y agoIn the absence of keyword arguments as a language feature, boolean arguments should almost always be avoided, and replaced by enums where possible. Boolean arguments are almost always inscrutable at the call site.
- skohan 6y agoThen it would be very convenient to have keyword arguments, as in the case presented by the previous poster
- atombender 6y agoIn Go (and, I think, Rust?) variadic arguments are often used for optionals: search("foo", CaseSensitive, FromStart) or options that take values: search("foo", WithCaseSensitivity(false))
- ridiculous_fish 6y agoIf this were a real API, you would need to add the trimmings: /// 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.
- simias 6y agoI see what you mean, but if that's the problem I'd argue that the weakness is that Rust lacks a shorthand to declare simple enums, not necessarily that it lacks keyword parameters. #[derive] soup aside I don't think too bad at the moment though, and I'm not really sure how you could simplify that without introducing too much magic into the language. 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.