Swift callAsFunction not a function

Swift 5.2 introduced callAsFunction which gives one the ability to call an instance of a Swift struct as if it were a function.

struct Something {
    func callAsFunction() {
        // print
    }
}

func test1() {
    let s = Something()
    s()
}

In effect this lets us treat an instance of Something as if it was simply a functional closure () -> ().

This is handy for all sorts of things, as already described in SE-0253.

This lead me to wondering, however, if I could pass an instance of Something to a property or parameter that was expecting a closure of of the same signature.

func test2() {
    let s = Something()
    let f: (() -> ()) = s // doesn't work
    f()
}

The code above doesn't work as Swift considers it to be a type mismatch. If you do the following and explicitly specify the function if will work.

func test3() {
    let s = Something()
    let f: (() -> ()) = s.callAsFunction // works
    f()
}

So the question is... if callAsFunction allows a struct instance to be treated as a simple function instance, should the assignment in test2 not succeed?

Why should I be able to treat it as a function, but I can't pass to or assign it as if it were a function of the same signature?

1 Like

I just posted about a very similar thing:

I never liked this callAsFunction thing.

I wish we had something more abstract, for instance:

struct Adder <T> {
    operator () (T, T) -> T {
        // ...
    }
}

I know it smells C++ but I find it quite elegant. :slight_smile:

6 Likes

This is absolutely stirring the pot, but: How is that different from callAsFunction? It’s a named member of the type with a signature that matches how the call site will look.

2 Likes

I don't see any syntactic difference between the following two member functions that would grant the second one special privileges.

struct Foo {
   func jibberJabber () -> Int {
      // ...
   }

   func callAsFunction () -> Int {
      // ...
   }
}
3 Likes

Fair enough, thanks for clarifying!

4 Likes

Personally, I'm always paranoid I'll make a typo and name it calAsFuncion and would get absolutely no helpful warning from the compiler about this. Even having this implemented as a marker Callable protocol or attribute would be an improvement over status quo.

6 Likes

You mean, other than the first time you attempt to use that constructed struct as a function and it fails to compile?

I'm not saying this won't cause any warnings or errors at all. What I'd like to see is a helpful error message at the place of expected callAsFunction declaration. An error message at the place of use means that it defers the discovery of the source of the error, and the compiler most probably won't be able to infer that I made a typo at the place of declaration.

1 Like

Not saying it's optimal, just a bit unrealistic to expect the compiler to figure out all possible misspellings of callAsFunction as well as figure out the distinction between that and, say, callingFunction or asFunction.

And especially when one can have multiple functions/signatures.

At any rate, all of that was debated long ago in SE-0253 so kind of spilt milk there...

1 Like

+1 from me to make it more consistent.

func foo() {}

struct Bar {
    @discardableResult init() {}
}

struct Baz {
    func callAsFunction() {}
}
var baz = Baz()

func test(_ execute: () -> Any) {}

foo()                       // âś…
Bar()                       // âś…
baz()                       // âś…

foo()                       // âś…
Bar.init()                  // âś…
baz.callAsFunction()        // âś…

test(foo)                   // âś…
test(Bar)                   // 🛑
test(baz)                   // 🛑

test(foo)                   // âś…
test(Bar.init)              // âś…
test(baz.callAsFunction)    // âś…
2 Likes

IMO the most obvious spelling would be func (). Not only does it draw a crystal clear connection between declaration and usage, it's also impossible to misspell and it clears up any confusion about why you can't treat the struct as a function itself — although this should be ultimately allowed.

struct Something {
    func () {
        print("I like somethings!")
    }
}

let something = Something()
something() // prints "I like somethings!"

struct Adder<T: AdditiveArithmetic> {
    func (_ a: T, _ b: T) -> T {
        a + b
    }
}

let adder = Adder<Int>()
adder(3, 5) // 8

[5, 4, 6, 2, 1].reduce(0, adder) // valid, returns 18
6 Likes

This is good. :sweat_smile:

An anonymous func an excellent idea. :joy:

In SE-0253, func() was considered as an alternative. I agree it would be clearer, with the only potential downside that the declaration (syntax) is a little less searchable online maybe. :man_shrugging:

Another nice feture about that would be that it's very similar to subscript (with the exception of argument labels though):

extension JibberJabber {
  func() -> String { "call as function!" }
  subscript() -> String { "subscript!" }
}
let jabber = JibberJabber()
print(jabber())
print(jabber[])
Side note

This symmetry could also make room for subscripts with a base name – without having to introduce a proxy property – such as:

extension Matrix {
  subscript row[i: Int] -> Vector { ... }
  subscript column[j: Int] -> Vector { ... }
}

I point this this because, coincidentally, the motivating reason this choice was not made was the need to be able to reference the member function:

let f: () -> String = jabber.callAsFunction
let g: () -> String = jabber // 🛑 I mean, why not?

…which was precisely the original topic of this thread. :smiley:

1 Like

Not a compiler engineer, but one possible reason is that jabber() is unambiguously a function call by syntax alone, whereas let g: () -> String = jabber requires type context to determine whether to assign jabber.self or jabber.callAsFunction to g. You can imagine how this gets more complex in situations such as:

let q = jabber // is type(of: q) == JibberJabber.self, or () -> String?

func identity<T>(_ arg: T) -> T { arg }
print(type(of: identity(JibberJabber())) // what is T, and therefore type(of: r)?

From my perspective, it should only be coerced when there's an explicit type that exactly matches the function type. Just like KeyPath<Root, Value> can be coerced to (Root) -> Value.

let q = jabber // type(of: q) == JibberJabber.self
let q: () -> String = jabber // type(of: q) == () -> String

func identity<T>(_ arg: T) -> T { arg }
print(type(of: identity(JibberJabber())) // T is JibberJabber
2 Likes

My vote would be for this symmetrical solution:

extension JibberJabber {
  func name(_ v: Int) -> String { "named function w param" }       // x.name(1)
  subscript name(_ v: Int) -> String { "named subscript w param" } // x.name[1]
  func name() -> String { "named function w/o param" }             // x.name()
  subscript name() -> String { "named subscript w/o param" }       // x.name[]
  func (_ v: Int) -> String { "call as function with param" }      // x(1)
  subscript (_ v: Int) -> String { "unnamed subscript with param" }// x[1]
  func () -> String { "call as function w/o param" }               // x()
  subscript () -> String { "unnamed subscript w/o param" }         // x[]
}
3 Likes

This is the first time I’m hearing of the possibility of func (_ input: Int) as a better spelling for func callAsFunction (_ input: Int), so I haven’t heard any potential downsides, but at first glance it sounds clearly better to me

1 Like

one big advantage of callAsFunction is you can reference it in documentation as JibberJabber.callAsFunction.

subscripts are not methods with square brackets, they are computed properties with arguments, and trying to make them look “symmetric” to method signatures would obscure their true nature.

named subscripts often feel like something we want, but they wouldn’t actually behave the way we expect them to, because a named subscript (with a set or _modify) would claim exclusive access over all of self. which means you can’t do something like

var foo:Foo
bar(updating: &foo.name[0], in: &foo.metadata)

which you would be able to do with a true nested view type with storage.

1 Like

Think this went off the rails a bit. Not wanting to argue the original syntax, still just wanting to know why I can't assign or pass an instance of a "functional" struct to something that expects a function?