Hello! I haven't participated on the Swift forums much, so please excuse me if I unwittingly break some unwritten rule or something.
Ever since the introduction of callAsFunction
with SE-0253, I've felt that it breaks certain conceptual consistency in Swift's syntax. Specifically:
- It adds a magic function name
- It introduces inconsistency between the different "callables" in the language
Let's go into more depth for each of these issues.
Magic Function Name
Take a look at this hypothetical structure:
struct MyStruct {
let myArray: [Int]
init() {
self.myArray = [1, 2, 3]
}
subscript(_ index: Int) -> Int {
return self.myArray[index]
}
func callAsFunction() {
print(myArray)
}
}
MyStruct
obviously doesn't do anything useful, but it illustrates well the magic-function-name issue. All three of init()
, subscript(_:)
, and callAsFunction()
are blocks of code at the top level of the structure that the compiler lets you call in special ways: init()
is executed when you instantiate the structure with something like let myStruct = MyStruct()
, subscript(_:)
is executed when you use the subscript syntax on instance of the structure with something like let myValue = myStruct[1]
, and callAsFunction()
is executed when you, well, call an instance of the structure as if it were a function with something like myStruct()
. The invocation syntax for all three is different from the standard syntax for invoking a method on an instance (or on a type in the case of static methods). Yet, while the declaration syntax for init()
and subscript(_:)
clearly indicates that they're not used like regular functions by omitting the func
keyword, callAsFunction()
has no such indicator. Indeed, the only thing that makes callAsFunction()
's declaration different from that of any standard, non-magic function is the fact that it happens to use the identifier "callAsFunction". There's no grammatical indication that it's compiler magic.
I've thought of two possible solutions for this, one of which is, I think, clearly better than the other. I'll explain the less preferable one first.
Magic Protocol
One possible solution could be to introduce a compiler-magic protocol, perhaps CallableAsFunction
. It's normal for protocol conformance to add extra "features" to a type, especially for protocols that come with predefined extensions. Here's how "conformance" to CallableAsFunction
might look:
struct MyStruct: CallableAsFunction {
func callAsFunction() { ... }
}
It's as if callAsFunction()
were a named requirement of CallableAsFunction
.
There are two main problems with this solution:
- The protocol is still magic
- It behaves differently than non-magic protocols with overloading
That second problem is particulaly, well, problematic. Today, you can overload callAsFunction()
as much as you want with different parameter and generic combos. If CallableAsFunction
weren't magic, then it would have to name infinitely many optional method requirements. This is obviously absurd, and that's without even getting into the fact that normal Swift protocols can't have optional requirements at all, let alone an infinite number of them. This is why I don't think that it's the right solution.
Syntax Unification
The better solution, in my opinion, is just to unify callAsFuntion
with the init
/subscript
syntax. It could look something like this:
struct MyStruct {
call(_ param1: Int, param2: String) { ... }
}
let myStruct = MyStruct()
myStruct(42, param2: "The meaning of life")
This makes it obvious that it's not any ordinary function, and the omission of the func
keyword that indicates that fact is consistent with init
and subscript
.
Inconsistency between "Callables"
This is the more complex issue. Let's assume for the sake of clarity in this section that neither of the proposals in the "Magic Function Name" section have actually been implemented in the language. Take a look at this code:
struct MyFunctor {
func callAsFunction() { ... }
}
typealias MyFunction = () -> Void
let myFunctor = MyFunctor()
let myFunction: MyFunction = { ... }
myFunctor.callAsFunction() // Works
myFunction.callAsFunction() // Compiler error
The fact that myFunctor.callAsFunction()
is perfectly legal but myFunction.callAsFunction()
isn't means that "under the hood", there are at least two distinct ways for a type to have its instances be callable. I think that this is a problem: calling something should be a singular concept, not two separate concepts. The most elegant way to resolve this issue would be to treat function types like () -> Void
as functors themselves that provide a callAsFunction
implementation that invokes the function's actual body. This could be—and probably would have to be—done entirely with compiler magic, but that's fine because it would mean that there's now only one conceptual way for something to be callable.
Conclusion
So, what do you think? Are these good ideas? Should I submit them as Swift Evolution proposals? If so, then should I combine them into one proposal or should I submit them separately? I would greatly appreciate any feedback or constructive criticism that you might have. Thanks for taking the time to read through this!