Search code examples
swiftlanguage-designnamed-parameters

Why does Swift require parameter names if it also requires argument order?


In Swift, when you call a function, you are required to label the arguments, unless the author of the function has explicitly allow you not to. Is there a reason for this in terms of language design? I had always viewed parameter labels as a good way to allow the caller to order the arguments in whatever way makes sense to them, but this way just feels like a lot of meaningless/useless boilerplate.

For example, if we define the function makeDog like this:

makeDog(legs: int, name: String)->Dog{}

Then it must be called like this:

makeDog(legs: 4, name: "fido")

And it can't be called like this (compiler error):

makeDog(name: "fido", legs: 4)

Even the Stack Overflow tag description for says:

Named parameters enable you to specify an argument for a particular parameter by associating the argument with the parameter's name rather than with the parameter's position in the parameter list.


Solution

  • TL;DR

    To help understand the answer to your question, you first have to understand that unlike other languages which support named arguments, in the case of Swift, you are not actually seeing argument names at the call-site. What you are seeing are known as outward-facing labels, and while they often match the underlying argument names, that is merely a coincidence/convenience. They are still very much separate things, with an entirely different set of rules, and understanding that difference is the key to answering your question.

    In short, outward-facing labels are words which are meant to add clarity at the call-site by helping it read more like a sentence, and just as you can't randomly reorder words in a sentence, you shouldn't be able to randomly reorder those labels either.

    The Rules

    A simple set or rules describing the above can be summed up as follows...

    • Outward-facing labels (external)
      These are meant make sense only at the call site so it's clear what you are passing. As such they may function more in a 'grammar' fashion rather than as an identifier. Unlike argument names, outward-facing labels do not have to be unique, and they can also be omitted entirely by setting the label in the method's signature to an underscore (_), which means 'Don't use a label for this argument.`

    • Argument names (internal)
      These are meant to make sense only in the context of the function/method's internal body. As such, these do have to be unique, and should be named according to the implementation of the function and not based on how it's called/used externally.

    As an example, consider a function that adds two numbers. Here's how it could be defined...

    func add(_ valueA: Int, to valueB: Int) {
        return valueA + valueB
    }
    

    And here's how you call it.

    let result = add(30, to: 12) // gives 42
    

    Above, valueA and valueB are the argument names which only exist inside the implementation for add. Additionally, the first argument omits its label (via the underscore in its place), and the second argument defines its label as to because that makes the call-site read like a sentence... 'Add 30 to 12'... making it very clear at the call-site what's going on.

    The Details (e.g. the reason)

    In most languages, there is no concept of outward-facing labels. You simply specify a function's argument values by ordinal position, usually separated by a comma.

    Now in some languages, like C#, in addition to specifying argument values ordinally, you can also/instead specify them by argument name. If you are specifying all arguments by name, it seemingly logically makes sense to be able to specify them in whatever order you want, but there are no requirements to use names so you can mix and match with ordinals.

    But if you can do that, what happens when you specify some ordinally and some by name? Now you're forced to put the named ones at the end because if you didn't, how would it know the position of the unnamed arguments? Or more confusing, what happens if you specify an argument value in the third position but you also specify that same argument by name later on? It can be confusing.

    In contrast, Swift strives to not only be much more consistent in its calling conventions, but it also strives to be a self-documenting language with a focus on clarity at the point of use (i.e. the call-site). As such, Swift added the concept of external/outward labels which default to being required, but it allows you to make them optional if it makes sense. And if you don't explicitly specify a label for your argument, the label is implicitly set to the same name as the argument, but again, they are still very different things.

    Now as to the why this is a good thing, let's look at some examples.

    Examples

    Let's start with a language that does let you reorder named arguments.

    var result = multiplyValues(valA: 20, valB: 5)
    

    Since you can reorder it, you can write this as well...

    var result = multiplyValues(valB: 5, valA: 20)
    

    Now in Swift, what you use at the call site aren't argument names, they are labels to help in the overall context of the signature itself.

    Consider this slightly more complex function definition...

    func multiply(value: Int, by factor: Double, thenAdd addlAmount: Int){}
    

    When called, it reads like this...

    let result = multiply(value: 20, by: 1.5, thenAdd: 5)
    

    Isn't that much clearer than the other example? But it's important to note that 'by' is not the argument name, but rather a label that makes sense given its position in the call. The actual, underlying argument is called 'factor' and that's what's used inside the body of the function's implementation.

    Following the Swift design guidelines, one may even make the first argument's label optional, which now reads like this...

    let result = multiply(20, by: 1.5, thenAdd: 5)
    

    Again, it's incredibly clear what's going on, almost reading like a sentence.

    Now imagine if you didn't use those labels and just used the argument names. Now the names matter in the external context. For instance, look at this

    let result = multiply(value: 20, addlAmount: 5, factor: 1.5)
    

    Clarity starts to get obscured. Are you multiplying 20 by 1.5 then adding 5, or are you multiplying 25 by 1.5? Is the answer 35 or 37.5?

    And what if you then used the external names/labels? Now it's even worse!

    let result = multiply(by: 1.5, thenAdd: 5, 20)
    

    What the heck is going on?

    When you reorder them in this way, it's not exactly clear what you're multiplying by 1.5, because if this is in an entity (struct, class, etc.) it could be easily mistaken that you're multiplying the implicit self by 1.5 (i.e. self.multiply(by:)), then there's some rando parameter 20 at the end without context. Again, it's not clear.

    Of course, naturally you may respond 'But that's with an optional label! The order shouldn't matter if and only if all labels have to be specified.' but now you're cluttering up the rules and breaking consistency for a single, specific use-case.

    Perhaps a more pertinent question is what do you actually get with that one-off use-case? With all these up-sides of the existing rules, what's a down-side? What are you not able to do by not allowing a random order?

    It's the above aversion to inconsistency that led Swift to introduce a breaking change in earlier versions of Swift where the first argument's label wasn't required even if you specified one. It implicitly added a _ if you will. But why should that first argument get special treatment when compared to the others? The answer... it shouldn't! All that did was confuse people, so they changed it in a future version because again, consistency is more important than cleverness, even over introducing a breaking change.

    Argument Names have to be unique. Label names do not.

    Since label names are meant to offer clarity at the call site, helping it read more like a sentence, it is possible to use the same label more than once since the order is also important.

    Argument names however must be unique because they need to coexist within the implementation.

    Here's a example demonstrating this. Note how the label 'and' is used twice, but the argument names 'a', 'b' and 'c' must be unique...

    func add(_ a: Int, and b: Int, and c: Int) -> Int {
        return a + b + c
    }
    
    let result = add(1, and:2, and:3)
    
    print(result)
    

    To Sum Up...

    External-facing labels should only make sense at the call site and should strive to complete the grammar of a sentence. Argument names however should only make sense in the context of a function's implementation.

    Follow those simple guidelines and it will become very clear what should go where and in what order, and why it's a good thing that you can't simply reorder your arguments at the call site.

    Bonus Topic - Like BK, 'Have It Your Way!'™

    Ok, so you now understand how important labels are to the Swift language, and you also get why order is important. But sometimes, you're just stuck with a poorly-designed API, one that makes absolutely no sense whatsoever. Maybe that's the reason you want to call arguments in any order you wish.

    As an example, say there's a package called BadMath that you don't own, don't like, but for reasons out of your control, you have to use in your project. Now say it defines an add function like so...

    class BadMath {
    
        func add(_ value: Int, to value2: Int, butNotBeforeFirstMultiplyingItBy valueMultiplier: Int) -> Int {
            return (value * valueMultiplier) + value2
        }
    }
    

    (Ugh... It's even painful for me to write that as an example!)

    So you're stuck using this awful API that clearly calls things in the wrong order, and your code too now looks like garbage as a result. And you don't like garbage code! You like clean code, and your code is normally stellar! But man... that function...

    Don't you just wish you could change it?! Wouldn't it be sooo great if you could clean that API up and make it work in a way that logically makes more sense to you? I mean... it would be really awesome if you could do something, say, like this...

    extension BadMath {
    
        func multiply(_ value: Int, by factor: Int, thenAdd addlValue, Int) -> Int {
            add(value, to: addlValue, butNotBeforeFirstMultiplyingItBy: factor)
        }
    }
    

    Oh, wait! You can, and that just did! 😁

    The point being, if you're using an API that you decide you really can't stand, and you really want to name and/or call the arguments in the order that you want, then go crazy with extensions in your own codebase and 'fix' their APIs!

    I personally do this all the time with Swift Packages, and even some of Apple's own frameworks.

    It's all in the goal of making the code more readable at the call site, so anything that works towards that goal is always the right... ahem... 'call' (Heyoooo! I'm here all week, folks! Tip the wait staff! 🍻 )