I’m experimenting with callAsFunction and noticed something about initializer parentheses:
struct Tester {
func callAsFunction(with block: () -> Void) -> Int { 5 }
}
let result = Tester { } // This compiles and runs: 5
At first glance, it feels like the initializer isn’t explicitly called — I’d expect Tester() before the trailing closure.
For comparison, these also compile:
let result = Tester()(with: {}) // No trailing closure
let result = Tester()() {} // Trailing closure
let result = Tester() {} // Trailing closure with function's "()" being omitted
Why is it valid to write Tester { } without the explicit () after Tester?
Is there a proposal or source reference that explains this behavior?
it appears that this construct did not typecheck prior to swift 5.7 (see this demonstration with a similar example), so i'm not sure whether it's intentional that it currently does. i can't quite divine the difference in the typechecker's constraint solving debug output, but it seems like in the older compilers it maybe always considered the closure as an initializer parameter, and if the types did not align, it errored, but in later compilers it considers Tester {} as containing an implicit call to the zero-parameter synthesized initializer followed by a callAsFunction invocation. perhaps @Slava_Pestov or @xedin could shed more light on the details and intentionality of this behavior.
confusingly, if the type itself contains a closure, it has varying odd behavior:
struct Tester {
var impl: () -> Void
func callAsFunction(with block: () -> Void) -> Void {
print("call as function invoked")
impl()
}
}
Tester { print("init or call arg?") }
// prints nothing
// but if you add a default value to the closure property...
struct Tester {
var impl: () -> Void = { print("default closure invoked") }
func callAsFunction(with block: () -> Void) -> Void {
print("call as function invoked")
impl()
}
}
Tester { print("init or call arg?") }
// prints:
// call as function invoked
// default closure invoked
i had tested it in a playground (some version of Xcode 26...), but here's a godbolt example that seems to have the same behavior (on the latest main branch snapshot): Compiler Explorer
I don’t remember exactly but I think it helps to support SwiftUI modifier types. We do allow parenthesis omission for normal initializer calls with trailing closures, so this is really no different, it just injects .callAsFunction like it would after ().
Hmm this seems quite different to me—in the case of TypeName { ... } there is never a clear invocation of the initializer so it is not at all obvious to me that there is an instance on which to invoke callAsFunction in the first place. The case of TypeName(...) { ... } there's clearly an instance on which we could invoke callAsFunction.
I vaguely remember that something like this came up in callAsFunction discussion, this is very similar to multiple trailing closures argument as well. We do allow skipping parenthesis and that’s why this works because Test { } means Test(<<defaults>>?) {} at the moment with or without callAsFunction.
Right, but I have always understood the model to be that you are allowed to skip (empty) parentheses for the call that receives the trailing closure. In this case we are omitting an entirely unrelated set of parentheses, for a prior call, merely because the type happens to implement callAsFunction. That doesn't seem particularly principled to me.
IOW, in my view, TypeName { ... } unambiguously attempts to invoke a TypeName.init and pass a trailing closure. TypeName() { ... } could be an invocation of TypeName.init or TypeName.callAsFunction.
I’m not sure what you mean, parenthesis are allowed to be omitted not because of callAsFunction but because it’s a type reference before the closure literal, Test() {} and Test {} are represented in exactly the same way - Test.init(){} because the solver even attempts to inject .callAsFunction anywhere.
My point is that the parentheses are omitted because of the trailing closure matching—the decision to transform Test {} into Test.init() {} is in my view, implicitly a decision that the trailing closure is indeed matched to the initializer. OTOH, Test() {} is genuinely ambiguous, because we could choose to either pass the trailing closure to the Test.init() call, or find an omitted set of parentheses for a callAsFunction invocation (Test()() { ... }) and pass the trailing closure to that call.
I unfortunately can't make any sense of what you've been saying here, but trying to figure it out did lead me to find that the related bug crashes the compiler when using .init.
I don’t think there is any disagreement on that, the solver gets exactly the same call representation in both cases is what I’m saying, and
indeed it used to be, and this is what the PR Hamish referenced was about. The solver would attempt to match closure to the initializer first and if that fails fallback to .callAsFunction if the type is callable to avoid this ambiguity.
I'm arguing that it shouldn't from a language perspective. The decision to expand Test { ... } into Test.init() { ... } should encompass a decision that the trailing closure is conclusively passed to the initializer. Otherwise there was no reason to expand the missing parentheses in the first place!
I understand you point but I cannot say that I agree that one should preclude the other because the subsequent transformation happens at a later point. This is just one example of how multiple features in the language interact in surprising ways.
Doesn't seem worth it. Calling initializers without parentheses is the same as calling methods without parentheses. The bug needs to removed from initializers, to match methods, which do not have broken behavior. Once this is all fixed, I don't suspect there will be a need to address the crashing issue.
(S.`init`()) { } // Compiles, as it should.
// 💥 Extra trailing closure passed in call
// I don't care much if this compiles.
S.`init`() { }
S { } // Compiles. Makes no sense.
// 💥 Extra trailing closure passed in call.
// If the above line compiles, then this has to as well,
// for consistency with this subset of Swift that is not Swift.
S.`init` { }
TSPL describes the rule for trailing closures and omission of parentheses as I've always understood it:
If a closure expression is provided as the function’s or method’s only argument and you provide that expression as a trailing closure, you don’t need to write a pair of parentheses () after the function or method’s name when you call the function
In other words you are allowed to omit the parentheses from a call when you are passing a trailing closure as the only (explicit) argument to a call. In the case of TypeName { ... }, if this invokes the instance's callAsFunction method, we've omitted the parentheses for a call (namely, the TypeName.init() call) which does not receive a trailing closure argument.
This sounds outdated because it means that multiple trailing closures shouldn’t allow omission on parenthesis either. Anyway, it sounds like this is something LSG should decide on that take into account possible source breaks changing this behavior would cause.