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).
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 :)
Do you have any feedback on that article's thesis that the closure syntax could have been handled better?
Every programming language changes as it evolves and matures and people understand how to use it best or its domain changes significantly. Unfortunately, this always means accumulating these syntactic and semantical warts. Personally, I think Swift manages to keep a good balance, and it's a very powerful language, with lots of users and compatibility requirements, so it's much harder for them than for many others.
Alternative is constant redesign of syntax and behavior that just keeps making users unhappy with every release, breaking their code, libraries and obsoleting the docs.
If, to mitigate that, you try to make big changes to the language in big releases, once in a long while, you can easily get into the Python 3, Scala 3, Perl 6 fiasco and that can simply kill the language adoption.
It's a hard problem.
As regards Perl 6, a recent development was to rename it to Raku, which (in hindsight) seems like a perfectly sensible decision. The overloading of the name Perl for a language that proved to be something different from Perl 5 caused trouble both ways. Perl 5 was widely considered to be a version that was to be eventually be replaced, whereas Perl 6 was considered a language that was a close and direct iteration of Perl 5. So we basically got the worst of both worlds. Perl 5 is very much in use today, still being actively maintained and is getting new features regularly, while Raku/Perl 6 is a beautiful new different language with interesting ideas being worked on by many brilliant folks, and the notion of trying to be in any way compatible with Perl 5 seems utterly irrelevant today.
I know they aren't big because Perl has had to deal with many of those same type of problems, only it didn't have to break old code to do it.
Perl 6 is more of a research project that took Perl 5 as a leap-off point. As it is is such a large departure from every language, it deserves it's own name. So it is now known as Raku.
I'm a bit perplexed by the author's position that Swift should just be small-talk as though this is obviously better.
As far as the idea of getting rid of parens entirely as the author suggests, I'm not a big fan of this approach. In languages like ruby where parens are not used, I tend to find this makes things a lot less readable. That might be a matter of opinion, but at the very least I think it can be said that Swift's approach to arguments, where only closures can be moved out of the parentheses of a function call is a perfectly consistent and defensible approach. For instance I don't think that this:
foo(someArg: 42) {
// do something
}
Is less clear than this: foo doSomething: {
do something
}
someArg: 42
In fact I think it's probably the opposite, and the only reason I can imagine to prefer the second example would be out of some appeal to purity/simplicity of the function call syntax over any level of pragmatism.As far as the whole debate over the inconsistency of the first argument label being removed while the following argument labels are required in multiple-trailing-closure syntax, I just don't think it's that big a deal. I also understand the consistency argument, that it should be handled the same for all arguments (i.e. the author of the function signature should explicitly decide if the label is required or not), and it probably should work that way, but I also understand why it hasn't been proposed that way for the first iteration. It would break a lot of code given how prevalent trailing closures are in Swift.
However there are already proposals in the pipeline to introduce an argument label in the first trailing closure (provided the function author hasn't named it `_`). I suspect in the long term this will eventually make it into the language, and automatic migration tools will make it possible to resolve issues with existing code without much fuss.
So in short I think multiple trailing closures are a good and natural addition to the language which provide real value, and the proposed syntax is absolutely fine.
Passing around objects is precisely the workaround people use for lack of named arguments.
In my opinion, languages should be simple and small, relating on composition of core syntax features over introducing new syntax for each and every new stylistic fad happens to be popular this year.
It cannot. Because, as you yourself said it: the function must know how to deal with object-as-parameters.
> relating on composition of core syntax features over introducing new syntax
This doesn't work for named arguments. They either exist in a language, and you can use them everywhere, or they don't, and you're left with inconsistently implemented workarounds.
And what I’m saying is that if they don’t exist in a language and you can instead using other more core things in the language to implement them rather than adding new syntax (complexity) to the language itself, that’s (in my opinion) a better language and the one I’d rather work in.
Once again: these are workarounds and there are no "functions declared with named parameters" in Javascript. There are functions that accept objects as a parameter, and they employ additional code to work with those objects.
Example:
function add(x, y) {}
No matter how much you try, you cannot "use the core thing in the language" to make this function accept this add(x = 1, y = 2)
// or
add({x: 1, y: 2})
without writing additional code inside the function itself.So if a library gives you `function add(x, y)`, that's what you're stuck with, forever. You cannot pretend that named arguments can be solved by "javascript's core language". It cannot.
Yes, there’s no dedicated syntax to allow people to name parameters that weren’t intended to be named. So what?
Of course it's not complicated, it's as simple as I showed.
> If I’m making a function that has potentially confusing parameters, I make it accept an object.
It doesn't really matter what you, a single developer, do, does it?
If I have to work with a third-party library that has "confusing parameters", please enlighten me how I can make its functions "accept an object to force named parameters"?
> there’s no dedicated syntax to allow people to name parameters that weren’t intended to be named. So what?
I've clearly shown what in at least three messages.
Keyword parameters are great. The way they are done in x.f() syntax languages is sub-optimal, and Swift is particularly weird.
> code becomes almost self-documenting
Yes, exactly!
Dan Ingalls explains this wonderfully in his Object Oriented Programming talk. Highly recommended:
https://youtu.be/P2mh92d-T3Y?t=515
The problem is that there is the Smalltalk/Objective-C style keyword syntax or the Pascal/C/C++ etc. function syntax. Both are consistent and complete ways of handling this, though I obviously think the Smalltalk way is better.
But it's either/or. When you jam them together, you get a weird mess of special cases and workarounds.
I would agree that the benefits of labels outweigh the messiness, even for the x.f() + labels case. So if you're coming from the x.f() side of things, I can fully understand and sympathize the view that they are a pure benefit without drawbacks. But once you have keyword messages, you no longer need the x.f() part of the syntax, because the syntax is completely self-sufficient. And then all the extra messiness of integrating the two makes no sense whatsoever.
As an example, the colon and the whitespace are sufficient to separate parameters. So is the comma. So we have...both?
There is also the issue that, please correct me if I got this wrong, the label that's in the signature becomes the name of the parameter inside the function. And as far as I can tell it cannot be changed/mapped. That's a horrible coupling of inside and outside.
Because of this, you actually need labels in your function declaration even for parameters that are unlabeled. Wut? And because of this, you need another special bit of syntax that says "despite the fact that I am now about to give a label, this is unlabeled". It also means that a name that is actually purely internal to the function is part of the signature. Wat?
And sometimes you get partial labels that just kind of sit there. It's really weird when you look at things closely.
I guess an analogy would be like a gasoline engine, a hybrid and a fully electric. Yes, the hybrid seems like an obvious improvement to the pure gasoline engine, if at some cost in complexity. But the pure electric car (let's assume a few more years improvement in battery tech) does away with all that extra complexity.
Hmm not entirely sure that analogy is all that good.
The way Swift does them is the least awkward and most robust I have seen in any language.
> As an example, the colon and the whitespace are sufficient to separate parameters. So is the comma. So we have...both?
Designing a language around what the minimum requirements of a parser is will not get you a good language. Human language is massively redundant, because humans find it more easy to communicate when there is redundancy. Computer code is also communication aimed at humans, and a bit of redundancy does not hurt there, and can often help.
> There is also the issue that, please correct me if I got this wrong, the label that's in the signature becomes the name of the parameter inside the function.
This is incorrect. You can optionally specify both an internal and external name for any argument. Unlabelled arguments are just a special case of this syntax, where you say "external name does not exist, internal name is this".
You got that wrong. You can specify a separate internal name like this:
foo(externalName internalName: Type) { ... }
> you actually need labels in your function declaration even for parameters that are unlabeled. Wut? And because of this, you need another special bit of syntax that says "despite the fact that I am now about to give a label, this is unlabeled"I mean you need to refer to those values inside your function as something and that has to be specified somewhere. How else would you do it?
Also the `_` you're complaining about is just a special case of the syntax I described above. This just means the external parameter name is omitted at the call site. It's quite consistent: if two names are given, one is external and one is internal. If only one is given, that means the external and internal names are the same (which is convenient since this is the common case).
> It also means that a name that is actually purely internal to the function is part of the signature. Wat?
Again, where on earth else would you put it? That's only also half true: the public signature does not include the internal parameter name. For instance I can declare a protocol like this:
protocol P {
func foo(arg: Int)
}
And both of these implementations will conform to it just fine despite the fact that the internal argument names are different: struct A: P {
func foo(arg internalA: Int) { ... }
}
struct B: P {
func foo(arg internalB: Int) { ... }
}
> And sometimes you get partial labels that just kind of sit there.Can you give an example? I'm not quite sure what you're talking about.
Ahh, my bad! But only the "cannot be changed/mapped." part, right?
foo(externalName internalName: Type) { ... }
Doesn't this strike you as odd at all? The part that looks like the label is not the label, it's the name of the variable. And then there's this extra bit in front that doesn't look like the label, doesn't really look like anything TBH, that will turn into the label. If present.I kind of see how they got there, but boy is it inconsistent.
It seems pretty simple to me: everything before the colon is defining the argument name. If you have one label, it means the argument label is the same as the name of the parameter used inside the function. If you have two labels, then the first one is the argument label and the second one is the variable name.
You could also think of it that you’re always specifying the argument label and the variable name, and in the case where they’re identical you have a bit of shorthand to save some typing.
Maybe it seems weird to some people if it’s not familiar, but I got used to it pretty quickly after starting to use Swift, and it felt like a natural extension of the type system and optional handling in terms of the language giving me tools to cross-check things and bake sanity checks into the source code itself.
Probably. ¯\_(ツ)_/¯
> everything before the colon is defining the argument name.
So the space binds more strongly than the colon. That seems..."unusual".
You can in fact provide names for both, and there at least used to be (I think they might have removed this?) some ludicrous special case on the behavior of the first parameter.
I think named arguments is an example of trying to solve issues at the language level, when the real problem is the information that is hidden from you while programming.
Think: f(fst:x, snd:y), what is the type of f(snd:4)?
I think this can be implemented, but then f(3, fst:5) is either an error or counter-intuitive.
In practice the order and identity of the arguments is quite clear in Swift.
Is there a good intro to Swift that goes through only what is unique/peculiar about Swift and explains that in a good way to someone that already knows how to program?
# let f ~fst ~snd = fst + snd;;
val f : fst:int -> snd:int -> int = <fun>
# f ~snd:4;;
- : fst:int -> int = <fun>Without overloading you could define the answer as
g(x) { f(fst:x, snd:y) }
always which would make partial application somewhat ergonomic but makes allowing fst without snd horribly ugly when you have to pass in some spiritual null or _ to.I think these ideas are fundamentally opposed and so I don’t think there’s a good way to have a nice syntax for both.
f(fst = _, snd = y)The one largest obstacle for partial application (at least of the Haskell style) is optional arguments. You just can not have both.
But yeah, functors are all about building stuff like that. So there are similar things, just with a different syntax.
> The already weird-ish addition of Smalltalk/Objective-C keywords inside the f(x) syntax: f(arg:x)
I'm a huge fan of named arguments in Swift. It's one of the things I miss most when working with languages which don't have it.
For instance, I do a lot of work with computer graphics, and in that case you end up with a lot of functions with a lot of arguments for things like configuration. With named arguments, code becomes almost self-documenting. Without them, I just end up using comments in their place, but of course then I don't have the compiler making sure this is correct.
It's a totally optional language feature, and adds a lot of value in some cases.