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

I'm curious to hear what people think about allowing the below code to compile and run:

func runOnboarding (sendConfirmationEmail: SendConfirmationEmail) async {
    await registerUser(
        using: sendConfirmationEmail(to:) // This is currently where the error appears: "Cannot find `sendConfirmationEmail(to:)` in scope"
    )
}
func registerUser (using confirmationEmailClosure: (String)async->()) async {
    // ...
}
struct SendConfirmationEmail {
    func callAsFunction (to recipient: String) async {
        // ...
    }
}

This can be made to work by writing:

await registerUser(
    using: sendConfirmationEmail.callAsFunction(to:)
)

but that defeats the beauty of Swift's callAsFunction feature, and it is possibly no longer worth it to me as the function author to choose to name the function callAsFunction if sometimes I'm going to have to explicitly reference that name in my business logic.

10 Likes

I sometimes use this in SwiftUI:

struct MyView: View {
    @Environment(\.dismiss) private var dismiss
 
    var body: some View {
        // ...
        Button("Close", action: dismiss.callAsFunction)
        //                              ^^^^^^^^^^^^^^
        // ...
    }
}
4 Likes

The first time I saw other people asking about it was here: Can function objects be passed as arguments for a closure-typed parameter?

Instance subscripts should work the same way. (I.e. when the verb is “get” or “set” instead of e.g. “send”.)

1 Like

Ditto. It can walk like a duck. It can quack like a duck.

But I can't treat is like a duck???

I don’t like ducks (or snakes), I like swallows! :smiley:

1 Like

Umm... this is the Swift forum.

There might be a swallow forum on Reddit... ;)

2 Likes

3 Likes

This feels similar to the issue where you can't bind a default argument when referencing a function as a value. For example,

func f(_: Int = 0) {}

let fn: () -> () = f // invalid

Or even that a metatype does not implicitly convert to a function type of its initializer.

Philosophically, do we expect that a things you can call convert to a function type, or that just functions themselves can be referenced as a "raw" function value without the other sugar that comes with a call expression? The latter is certainly much simpler to implement and reason about, but there's room for debate here.

11 Likes

Another piece of the puzzle to consider in the philosophical debate is that we allow a key path literal with base T and result U to convert to (T) -> U since it 'acts like' a function, but of course the actual application syntax for a keypath looks nothing like a (T) -> U function application.

3 Likes

It's worth mentioning that a bare metatype isn't allowed at all in expression position; most of the time when I try, it's because I wanted .self, not .init. In fact, there's currently an autofix for that:

struct Foo {}
let x = Foo // autofix: change to Foo.self

and I wouldn't want that to go away.

3 Likes

Usually, newly proposed syntax needs to have its semantics explained in the pitch (a simple ... obviously does not explain the vast complexity underlying the behavior of variadic generics), but in this case I think that we're all 100% clear on how my proposed spelling would behave and I basically didn't have to write any words at all explaining it, which I think is a evidence that there isn't going to be some other meaning in the future that we would wish to ascribe to my proposed spelling, and therefore it seems to me to be a safe change on that front. I don't know what parsing or other difficulties it could introduce.

Regarding the philosophical approach to this question, what do you guys think of conceptualizing callAsFunction as the "magical disappearing function name", the behavior of which is that everywhere where a function name can appear it can also be elided if the name happens to be exactly callAsFunction. The only exceptions would be implicit self method calls, where the callAsFunction can't be elided if self is elided, and global functions named callAsFunction which cannot elide the name either, (EDIT: a third exception is that (for now) static functions cannot elide callAsFunction, as pointed out by @bbrk24 ).

Would this change the current semantics of static methods named callAsFunction? Currently, any unqualified attempts to call them always refer to init, even if no init exists:

enum Foo {
    static func callAsFunction() {}
}

let x = Foo() // error: 'Foo' cannot be constructed because it has no accessible initializers

Obviously, you can still say Foo.callAsFunction(). I just wanted to ensure your proposal wouldn't make the above legal, since you didn't list it as an exception.

Ah, good point. I have wanted this ability in the past, but in reality I think there are better ways (or at least we could implement better ways) to solve what I was going for. What this would allow is for expressions that look like initializations of one type to actually resolve to another type:

func demo () -> String {
    Int(bool: true)
}
extension  Int {
    static func callAsFunction (bool: Bool) -> String {
        ""
    }
}

which is precisely why I wished it were possible, but again that's a big change and I'm not convinced that it's the right way, so I'll update my list of exceptions.

A change like that would allow for overloads that aren't technically redundant but impossible to differentiate:

struct Foo {
    var x: Int

    private init() {
        self.x = 0
    }

    init(x: Int) {
        self.x = x
    }

    static func callAsFunction(x: Int) -> Foo {
        var retval = Foo()
        retval.x = -x
        return retval
    }
}

_ = Foo(x: 1) // how do you begin to diagnose this?

Can't we just say at the callsite that it's ambiguous? This already happens with clashing method names in protocol extensions.

How could that ambiguity arise in Swift currently? Is it possible to define two methods with the same name, argument labels, argument types, and return type?

You can define two generic methods with different requirements that are both satisfied by the types at the call site for example:


func f<T: P>(_: T) {}
func f<T: Q>(_: T) {}

struct S: P, Q {}

f(S())
1 Like

It doesn't affect parsing, but it introduces a new implicit conversion (from types with a callAsFunction method to function types) and we're generally wary of adding more implicit conversions since they can contribute to exponential type checking.

There is also a new possible ambiguity which might be mostly theoretical:

class C {}

class D: C {
  func callAsFunction(...) {}
}

func f(_: C) {}
func f(_: () -> ()) {}

f(D()) // which conversion is better, the upcast or wrapping it in a closure?
1 Like

Swift converts a key path literal to a function. It doesn't convert a key path value to a function.

func f(_: (Array<String>) -> Int) {}
f(\Array<String>.count) // ok

let kp: KeyPath<Array<String>, Int> = \Array<String>.count
f(kp) // 🛑 Cannot convert value of type 'KeyPath<Array<String>, Int>' to expected argument type '(Array<String>) -> Int'
1 Like

Ah yes, of course, thank you for the correction :slight_smile: