Pre-RFC: Named arguments

Counterargument: Swift. Literally all of Apple SDK APIs use named arguments.

Named arguments from the beginning (and crucially, ones that are required) are objectively a useful tool for API design. There are edge cases to consider that do catch people out (e.g. function types and closures) but combined with a helpful compiler and an API design language that universally uses function argument labels, they serve as an enormous boon for quickly digesting an API surface.

Function argument labels are so important that even in languages that don't use them on calls, documentation typically shows them anyway. Rust trait functions used to be declarable without argument labels, but that usage is deprecated now. Public visible function argument labels are objectively a good thing.

Adding argument labels to an existing language is problematic because using argument labels leads to a drastically different API design language. Implicitly optional argument labels are a terrible idea, especially when argument names weren't an API guarantee previously, because they're so easy to accidentally make a poor name part of the stable API.

But there is nothing intrinsically bad about function calls using argument labels in a static language, (so long as they are a controlled part of a library's API,) because having them is no different API/ABI-wise than putting them in the function stem.

11 Likes

Swift is not a good counterargument at all: Swift has to be compatible to the prexisting platform APIs from Objective-C which was essentially a reimplantation of Smalltalk on top of C syntax. Smalltalk is a dynamically typed language and Objective-C leaned heavily on the same design and semantics.

I also did not say that naming parameters properly is a bad thing or that named arguments are always absolutely evil :person_facepalming:. I'm agreeing with most everything you said - Rust has already a naming scheme and we should not have two. Named arguments simply do not fit with the tradeoffs already made by Rust. They fit Swift due to its history. They fit python and other dynamic languages very well.

Lastly, while I agree there is a minor edge case where named arguments results in better API design (e.g having a non commutative function with two parameters of the same type) I still stand by my previous assertion that more generally, named arguments are somewhat inferior overall compared to relying on types because the ordering of parameters becomes more of an issue. Smalltalk has somewhat redundant APIs due to this. IfTrue:IfFalse: vs. IfFalse:IfTrue: (the concatenated names of the parameters form the function name in Smalltalk)

I've done some coding with Smalltalk-80 in the past and this was my experience. I still prefer to have traditional functions with few parameters (say 3-4 tops) and at $job we have a linter that checks exactly that. When adding overloading to the mix, the costs of named arguments greatly outweigh the benefits. Though even without it, you still have other costs such as having multiple ways to name the same thing. Also as I said before, making this a bit salty encourages splitting up functions so they won't have many parameters which results in better designed code overall.

We're literally going in circles here as I'm repeating myself from up-thread..

2 Likes

This is demonstrably false.

Swift needs to be able to call preexisting platform APIs. It does not need to and in fact doesn't call them by their existing names. It can and does include glue to rename every single preexisting API from the existing API design language into the new one.

That Apple was able to do this alongside all of the other impossible things Swift does (e.g. a stable ABI in the face of library evolution) is a marvel of the amount of developer time they threw at the problem.

Swift/ObjC is a bindgen+annotations problem. Swift could have abandoned named argument labels in the transition (and would have; Apple was not afraid to make sweeping breaking changes early on, and did make fundamental changes both to how they worked and how ObjC was mapped) if they were a poor fit for a static language.

5 Likes

It does so using a consistent pattern though AFAIK. If swift didn't support named arguments, the function names themself would need to include the argument names to avoid conflicts between for example -[NSString initWithFormat:arguments:], -[NSString initWithFormats:locale:] and -[NSString initWithFormats:locale:arguments:].

2 Likes

Other people disagree with your claim that it is isomorphic, and they give arguments for that counterclaim. You can't win an argument by just repeating what you said before.

Concretely: if this is isomorphic, then there should be something like this and this in Rust, after being transported through the isomorphism. So, where in Rust do we have the equivalent of these rules?

3 Likes

There's been several reports from both sides of the issues here. I'd like to encourage everyone to take a few steps back.

For @Ichoran and @yigal100 in particular, please try to focus on how this RFC pertains to Rust. RFCs can be supported by existing art, but it's not productive in the pre-RFC phase to argue against the proposal unless there is hard, concrete, and unavoidable reasons that the RFC shouldn't be considered. It's the point of the RFC phase for review for acceptance. Pre-RFCs should be focused on the drafting, making sure the RFC as a proposal is well written, even if you disagree with the proposal.

20 Likes

Swift Developer Here.

It's unfair to call the syntax unintuitive and boilerplate-y if you don't even understand it. Please read the documentation for functions in Swift here. The syntax is very simple:

func foo(param: Int) { }
foo(param: 5)
func foo(_ param: Int) { }
foo(5)

As this example shows, the underscore simply indicates that the parameter has no argument label (the identifier used by the caller). It's quite a stretch to claim that the addition of TWO characters (the underscore and the space) is boilerplate-y.

5 Likes

Calling it unintuitive is fair, IMO, since it is not something one will have seen in other languages. So it makes Swift code less readable for people coming from other languages. Of course all languages have things like that, but doing it for something as common as argument types is not necessarily a good idea.

6 Likes

Just a note: boilerplate doesn't need to be long. If you write the same thing every time (and @digama0 apparently had no recall of ever seeing anything but), then it's boilerplate.

Mostly, boilerplate is communicating information that you're comfortable assuming, but the other systems involved aren't, because they're supporting things that you aren't considering. Thus, you consider it boilerplate ("I just have to write this for no reason") while from a different perspective it's justified.

mut for bindings in Rust is boilerplate. It would be completely possible for the compiler to just make all bindings mut and nothing would change. Except things would change... but only if you're familiar with what leaving off the mut means.

What really makes something boilerplate-y is if the normal case requires writing more than the uncommon case. If the common case is unnamed arguments (as in the code @diagama0 had been exposed to), then specifying that the arguments should be unnamed is boilerplate. If the common case is that arguments are named (as is the main Swift naming conventions), then writing arg arg is boilerplate. This is IIRC a change that was made in Swift development, in that the first function argument used to be unnamed by default, and this was changed to match other arguments to avoid the need to write arg arg to get a named first argument.

It's still the case as I recall that most first arguments (of free functions) are still unnamed[1], so you could argue that requiring them to be _ arg is boilerplate, where the language could default to the common case, or you can argue that consistency is better in this case. Both are supportable positions.


  1. verb(_ object: Type, preposition object: Type)... though this is perhaps better written as (object: Type).verb(preposition object: Type)? ↩︎

12 Likes

This is one component, but this alone is a terrible definition. Boilerplate refers to something much more specific than mere repetition. Based on this, every single construct in the language is boilerplate, assuming it occurs more than once in your program. Every keyword is boilerplate; types are boilerplate; enums are boilerplate; variables are boilerplate; control flow is boilerplate; ownership is boilerplate; references are boilerplate. And it doesn't matter how these constructs are used; the mere fact that they occur more than once in your program makes them boilerplate, according to this definition.

I mostly agree that this principle is part of the definition.

However, this principle does NOT apply to mut. Consider the following example:

let mut y = 10;

mut is not "communicating information that you're comfortable assuming". mut communicates that y is mutable; there's no way to assume that y is mutable without adding mut. And there is a reason you have to write this: it's to tell the compiler that the variable is mutable ("I just have to write this for no reason").

Now consider this example:

let point: Point = Point::new();

In this example the explicit type annotation does "[communicate] information that you're comfortable assuming". A human can already assume that the type of the expression Point::new() is Point, so the the explicit type annotation is boilerplate in this case, and exemplifies the aforementioned principle.

Yes. And in that case, explicitly adding mut would be boilerplate. But the compiler doesn't do this, so I fail to see your point.

This makes no sense. If the compiler made all bindings mut, then the program will change whether you're familiar with what leaving mut off means or not.

No, this has nothing to do with whether or not something is boilerplate-y. I don't know where you got this from. Can you cite references for this claim?

What Is Boilerplate Code

Wikipedia defines it best:

In computer programming, boilerplate code, or simply boilerplate, are sections of code that are repeated in multiple places with little to no variation. When using languages that are considered verbose, the programmer must write a lot of boilerplate code to accomplish only minor functionality (emphasis added).

I encourage you to read the full article. In particular, read about the history of the term.

The example on java sums it up well:

In Java programs, DTO classes are often provided with methods for getting and setting instance variables. The definitions of these methods can frequently be regarded as boilerplate. Although the code will vary from one class to another, it is sufficiently stereotypical in structure that it would be better generated automatically than written by hand. For example, in the following Java class representing a pet, almost all the code is boilerplate except for the declarations of Pet, name, and owner:

Java

public class Pet {
    private String name;
    private Person owner;

    public Pet(String name, Person owner) {
        this.name = name;
        this.owner = owner;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Person getOwner() {
        return owner;
    }

    public void setOwner(Person owner) {
        this.owner = owner;
    }
}

Most of the boilerplate in this example exists to fulfill requirements of JavaBeans. If the variables name and owner were declared as public, the accessor and mutator methods would not be needed.

To reduce the amount of boilerplate, many frameworks have been developed, e.g. Lombok for Java. The same code as above is auto-generated by Lombok using Java annotations, which is a form of metaprogramming:

@AllArgsConstructor
@Getter
@Setter
public class Pet {
    private String name;
    private Person owner;
}
2 Likes

But this means that we can't use named arguments when we are destructuring a struct directly on the arguments, which makes some kinds of functions unergonomic.

So, I quite liked this:

And it's more pronounced if you are writing functions that receive two or three points (some problem domains where Rust is applicable may have lots of functions like this, eg. code dealing with linear algebra)

It also addresses one concern regarding named arguments: that it makes the parameter names stiff: previously they would be changeable as we were developing the function, and now changing them is a breaking change.

On the other hand, it's better to trim down the proposal; this is the kind of thing that can be added later!

Using types purely for the purpose of associating a name with a parameter at the call-site is heavy-handed. That's the point @gbutler made. No one said anything about using types in general being heavy-handed. This is a strawman argument.

1 Like

And the exact same thing applies to using custom types for parameters: A typo in the name of the type would be part of the interface.

Any means of associating a name with a parameter at the call site introduces that name into the interface of the function.

I couldn't agree more. It's interesting to note that the builder pattern generally only appears in languages without named parameters. In fact, when named parameters do exist, it's hard for me to think of reasons to use the builder pattern instead.

2 Likes

I can see the builder pattern being used when there more than 3 or 4 argument. Named arguments are not (to me) appropriate to a Struct::new() function taking 8 parameters or when the parameters are expected to be long/repeated.

An example is the Command type from the Rust standard library. A builder pattern works nicely for it, and while named arguments could be used in some parts, overall I don’t think they would be a great fit for all the functionalities that are available.

My goal is not to replace any one thing entirely with named arguments but to improve clarity/usability for function calls with few arguments, something which is forgotten by almost all the languages I have ever seen.

1 Like

Why does the mere existence of lots of arguments suggest to you that the builder pattern should be used?

Functions taking « lots » of arguments often have more complicated patterns that would drown named arguments. That’s not always the case of course, just a feeling I have from my experience.

But then there are counter examples, especially domain specific ones, like maths, where some functions take a lot of argument and a builder would not match the maths. There named arguments are a nice to have, even with 8 parameters (especially since in maths it’s common for several of the parameters to have the same type)

1 Like

And sometimes they don't. My question was: Why does the mere existence of lots of arguments suggest to you that the builder pattern should be used?

From my experience, the more arguments there are, the higher the probability of some of them being optional/having widely used default, eg. port 22 for SSH when not specified.

A function building an HTTP request for example can take a whole heap of headers, a body, a port, a url. Not all those are equal in importance and with named arguments I would probably design it with a builder like this RequestBuilder::new(url).port(1245).body("have a nice day").compressed(false).auth_header(auth).lang_header(lang).header("recipient-name", value: "rustacean").header(/* other non usual header */) … .build()

Having all those in one function call would mean the port and unusual headers would be None most of the time, with is not good ergonomics to me (but you can disagree and that and not be wrong, that’s my opinion, not a fact)

1 Like

Optional arguments. Mutually exclusive combinations of arguments. Passing a particular argument in multiple different forms (e.g. one method accepting impl intoIter<Item=T> and another called multiple times with a T each).

3 Likes