Function Trailing Labels As Void Parameters

Hello, Swift community!

I'd like to gather feedback regarding a feature that I'd like to see in Swift.
It's about allowing functions to define a trailing label (which, on the ABI level is a Void-parameter) to reduce visual noise and achieve function naming consistency:

Before:

struct Foo {
    init() { }
    init(forSpecialPurpose: ()) { }
    func isNumber(_ some: Int, greaterThan other: Int) -> Bool { some > other  }
    func isNumber(_ some: Int, greaterThanZero: ()) -> Bool { some > 0 }
}

let foo1 = Foo()
let foo2 = Foo(forSpecialPurpose: ())
assert(foo1.isNumber(2, greaterThan: 1))
assert(foo2.isNumber(3, greaterThanZero: ()))

After:

struct Foo {
    init() { }
    init(forSpecialPurpose) { }
    func isNumber(_ some: Int, greaterThan other: Int) -> Bool { some > other  }
    func isNumber(_ some: Int, greaterThanZero) -> Bool { some > 0 }
}

let foo1 = Foo()
let foo2 = Foo(forSpecialPurpose)
assert(foo1.isNumber(2, greaterThan: 1))
assert(foo2.isNumber(3, greaterThanZero))

Details

Older versions of Swift will recognize this as a parameter of type Void, so this feature is backward-compatible.
If the parameter preceding the trailing label is of a function type, then the trailing closure syntax still applies to it:

func shutDown(completion: @escaping (Error?) -> Void, gracefully) {
    // ...
}

shutDown(gracefully) { error in
    // ...
}

There is a lot of bikeshedding potential here so any and all comments and suggestions are very welcome!

2 Likes

I've sometimes thought something like this could aid readability, but it also has the potential to make subtle differences in behavior harder to spot.

More importantly though the trailing label looks like an unlabelled parameter, which could cause issues both for readers and also the compiler if a variable with that name already exists.

2 Likes

I don't understand what kind of problem this solves. It also makes the language more complicated because it's not clear at a glance whether (in this case) greaterThanZero is a property that's passed to the function's unlabelled argument, or whether it's a trailing Void.

The example in details is also rather strange because it looks like you're calling shutDown with what I can only assume to be a Bool named gracefully. Instead, it's something else entirely.

Having function arguments that are Void is something that I doubt is useful in any case, and I don't think we should create confusion in the language for a niche example with questionable usefulness.

2 Likes

I didn’t think of that, that’s a good point! What if we put the trailing label (at call site) outside the function call parentheses?

let foo1 = Foo()
let foo2 = Foo() forSpecialPurpose
assert(foo1.isNumber(2, greaterThan: 1))
assert(foo2.isNumber(3) greaterThanZero)

shutDown() gracefully { error in
    // ...
}

This seems a bit too magical, but at least it solves the ambiguity problem.

They are most useful in initializers. In fact, ExpressibleByNilLiteral uses one:

protocol ExpressibleByNilLiteral {
    init(nilLiteral: ())
}

This is starting to look like a more general problem of extending the Swift's core syntactic structure to accommodate more naturally readable code.
I'm not suggesting AppleScript-level stuff (which is actually very hard to understand, in my opinion), but definitely a step in that direction.
I do understand that this is not a big deal at all, but nonetheless is worth exploring in my opinion.

I'm not convinced this idea is useful to the language as a whole. The idea in my mind remains very niche and it introduces a weird kind of ambiguity that does nothing more than confuse me.

This example:

let foo2 = Foo() forSpecialPurpose

Looks even worse to me. It feels like a change to the language for the sole purpose of making a change without actually making the language better.

3 Likes

Placing the label outside the parenthesis looks even worse, imho. I'm not sure there is a way to do it that wouldn't be really confusing. If you're desperate, you could always use a fluent interface...

assert(foo1.isNumber(2).greaterThan(1))
assert(foo2.isNumber(3).greaterThanZero())

I’m huge +1 om this! I’ve started drafting the very same proposal many times myself but not managed to come up with a good enough example.

I’ve been wanting this since before Swift, in ObjC even.

It will really help make APIs even more easy to read.

Not sure about implementation of using Void though. I would like to see support for some new special statement, being a trailing label, but without a type. Not sure how hard that would be to implement though.

Historically I’ve been using special purpose enums - often with a single case - to provide the “trailing label”.

You can use an enum instead, to lose the '()' and for even less boilerplate/visual noise, for the condition tests:

struct Foo {

enum Condition {
	case greaterThanZero
	case greaterThan(Int)
	
	func test(value: Int) -> Bool {
		switch self {
		case .greaterThanZero:
			return value > 0
		case .greaterThan(let compare):
			return value > compare
		}
	}
}

init() { }
init(forSpecialPurpose: ()) { }
func isNumber(_ some: Int, _ condition: Condition) -> Bool { return condition.test(value: some) }
}

let foo1 = Foo()
let foo2 = Foo(forSpecialPurpose: ())
assert(foo1.isNumber(2, .greaterThan(1)))
assert(foo2.isNumber(3, .greaterThanZero))
4 Likes

This has been discussed at least a few times before, with intelligent comments. I find it can be helpful when posting new ideas to use the search function and summarize what’s already been said.

Chiefly, it seems no one has come up with a spelling that is both unambiguous (i.e., not confusable with something else) and not totally alien (i.e., calling for uses of punctuation in ways never seen elsewhere in the language).

1 Like

Note that that protocol is not meant to be used at all, and more so the initializer.

1 Like

Sometimes they come up in generics, like when the argument of something is the return type of something else...and that happens to not return anything.

I do also use them as the original poster here does though, for example:

My most frequently used CutPath constructor is:

init(id name: String? = nil, subdivider: Subdivider? = nil, allocationExcluder: AllocationExcluder? = nil, rectMesurments: RectMesurments? = nil)

My fairly uncommon, but absolutely useful one:

init(id: String, subdivider: @escaping Subdivider, subdivideOnly: Void)

On the other hand I don't find it a large burden to call it as

CutPath(id: "Movement Tray", subdivider: someSubdivider, subdivideOnly:())

It would be a bit nicer, and maybe not syntactically ambiguous to omit the empty parens though.

But what is the purpose of such an argument? Why not a Bool? Or something more meaningful? It seems like the presence of the Void here means that you want something to happen, and if it's nil you don't want it to happen? A Bool or enum seems more fitting for that kind of thing.

The subdivideOnly:false case is already handled by the other constructor, and is the overwhelming majority of uses. So making the common case "option: false" reads poorly. Making it the final argument of the other constructor lets you create invalid combinations of options ("this set of paths can ONLY be used indirectly by breaking into parts, but if not broken apart the edges are described thusly, and the following areas are unused and some other part can be cut in them").

I could use an enum with only a single case, but "subdivide: .only" isn't really different from "subdivideOnly:()". The enum taking version has the minor burden of implying that options other then ".only" exist, or at least might exist in the future. The Void taking one loos a little odd (but I'll argue that is more because we seldom write them then any inherent awkwardness).

Bit of a digression, but for historical interest, this was asked about on Stack Overflow and got a response from none other than Brad Cox! (As well as Bill Bumgarner, who pinged him about it.) selector - Why must the last part of an Objective-C method name take an argument (when there is more than one part)? - Stack Overflow

4 Likes