Pitch - Allow eliding `callAsFunction` when forming a reference to the function rather than calling it

Huh, I honestly would've expected you to be able to say f(S() as P) or similar, but apparently you can't. I've never run into this in practical code before.

In any case, you can (since Swift 5.1) disambiguate like this:

func fP(_ s: S) {
    let x: some P = s
    f(x)
}

func fQ(_ s: S) {
    let x: some Q = s
    f(x)
}

To put a fine point on it, the spelling of this feature is actually very clear about the current scope of the behavior: call as function—it is not behave everywhere as function, and the core team explicitly rejected an alternative “disappearing function name” spelling such as func _ ().

To my knowledge, the design of Swift is such that there are currently no implicit conversions to a function type. (As @mayoff has clarified, key paths are not convertible to functions; rather, functions can be expressed by key path literals, which is quite a different thing, just as Double values can be expressed by integer literals but very much do not implicitly convert from values of integer type.)

I am not so sure from either a type checking performance perspective or a language design perspective that it would be wise to go down that implicit conversion route, particularly since the desired functionally can be expressed either by (a) referring to callAsFunction explicitly; or (b) passing a closure where the body actually, you know, calls the value as a function.

5 Likes

I wrote this response but by the end of writing it I realized something important that I was missing:

Obsolete Response

I've been surprised to hear so much talk in this thread about this notion of implicitly converting one type to another. Firstly, I acknowledge that I know basically nothing about how the current callAsFunction feature is implemented. Can you help me understand what is fundamentally more "implicit conversion" about deriving a function reference from a type as compared to calling a function via that type?

I can't argue with your point that the feature is indeed named callAsFunction and that the function-ly behavior that I'm wanting to do is more like referenceAsFunction or something, but I think that's a separate question from the type-checker performance question. On this separate point about the naming, if the original intent had been to provide both callability and referenceability, is there a perfect word that would have captured both?

Back to the type-checking question - for what it's worth, here's why I imagined that this change would not imply any particularly large or fundamental shift in the compiler:

  1. Currently, already, if the name of a function is followed by an opening parenthesis then either it is being called or it is being referenced.
  2. Currently, if a reference to a nominal-type instance is followed by an opening parenthesis then it must have a callAsFunction method which must be being called, otherwise it is malformed.
  3. Since we already distinguish perfectly well between when a function name being used to perform a call versus to form a reference to the function, and since we already parse references to nominal-type instances that are followed by an opening parenthesis as if they were functions, shouldn't it be rather straightforward to perform the same check (function invocation vs. function reference) for nominal-type instance names that are followed by an opening parenthesis as we do for function names?

--End of Obsolete Response--

I was questioning why this change would have any particularly large ramifications on type-checking, and then I realized that since I was deliberately using argument labels in my use case I wasn't considering the idea of treating a bare reference to the instance itself as equivalent to a closure with no inputs (if the type declares a callAsFunction with no inputs). Am I correct that this particular issue is the source of the concern about type-checking?

If so, consider this option + rationale:

In my obsolete response I detailed why I imagined that this change would make relatively few waves in the compiler, but I was only thinking of forming the reference with parentheses. Leaving aside the inconsistency for a moment, would the implementation concerns be solved if that particular usage (bare reference implicitly converting to closure) were not supported, but references which involve parentheses were allowed? On the surface this seems potentially quite reasonable to me, mainly due to the combination of that one can always write bareReference.callAsFunction, and how much value I believe can come from being able to form references with parentheses without having to completely mess it up and muddy how it reads by sticking a .callAsFunction in the middle of the reference.

I also feel that callAsFunction and the old string-based dynamic member lookup features were mistakes of language design. This was clear to me at the time they were proposed, and in hindsight even more so. My person opinion is probably not the best idea to complicate them further.

I find this to be a very interesting perspective. Do you have a theoretical other feature in mind that would solve the problems that callAsFunction tries to solve but in a better way?

I began using callAsFunction recently for the purposes of dependency injection. I want to write my logic using “closures” that allow me to swap out dependencies, but I also want the “production” and testing variants of these closure dependencies to be available as static members, which is why it was very useful to be able to swap my closures for structs that have callAsFunction and static members.

Would you have preferred a feature that makes closures more like nominal types rather than one that makes nominal types more like closures?