SE-0253: Static callables

  • What is your evaluation of the proposal?

I think it's a great feature, but I don't see why it's implemented using a new declaration instead of func call(…) + an attribute on the type.

New kinds of declarations have a fairly large impact on the language. Their interactions with other features need to be explicitly defined:

  • Can you declare a call...
    • …at global scope?
    • …in a local function?
    • …in an extension?
    • …in a protocol?
    • …in a protocol extension?
  • Can you inherit them from a superclass?
  • Do calls have…
    • …return types?
    • throws?
    • …a failable ??
    • …access control modifiers?
    • mutating modifiers?
    • static modifiers?
    • override? final?
    • …attributes? (Which ones?)
  • Does foo.`call` refer to a call member?
  • How are they represented in mangled names?
  • How are they represented in runtime metadata?
  • Can you fetch one as a bound method? An unbound method? A key path?

The proposal answers (at least most of) these questions; that's not my point. My point is, implementing those answers requires changes to all of our tooling, changes to the ABI and runtime, and changes to tools maintained by volunteers or companies using Swift. And it will need to be considered in future work on features like reflection and macros. In short, it turns this proposal into a much bigger extension of the language than it really needs to be.

Existing decl + attribute has a much smaller impact and implementation effort; it's a better solution if an existing decl's behavior is close to the desired behavior. Here, the only difference we really want between call and func declarations is that we don't want static func call to work. The type checker can easily check that it's not working on a metatype before considering a call function, and the decl checker can just as easily detect and diagnose a static func call in an @callable type, so that's easy to support.

Maybe there's a strong reason to design it this way. For instance, if you imagine that the built-in structural function type could one day be treated as having a call declaration and that this would yield significant type system improvements, that could be sufficiently beneficial to justify the complexity of a separate call declaration. But right now, I just don't see it.

The implementation bears this out. Scroll through the diff and try to get a sense for how much of it actually implements the desired behavior compared to how much teaches various parts of the compiler that call declarations exist. My guesstimate is that only 200-300 of those lines implement and test the type-checking behavior or necessary declaration checking; that leaves 900-1000 lines that only exist to support the new kind of declaration.

I urge you to think about how much shorter and simpler the "Proposed solution" and "Detailed design" sections would be if you were proposing func call + @callable [class/enum/protocol/struct] instead of this new call member and to make your decision accordingly.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes—it would be very useful to make pseudo-functions that are configurable, serializable, or equatable, and I can think of tons of uses beyond that.

  • Does this proposal fit well with the feel and direction of Swift?

Yes. This is far more Swift-y than @dynamicCallable.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Call syntax is one of the few things that can't be hooked in Ruby and the language suffers for it. Even the built-in types for functions have to overload subscripts or expose .call methods. It's a mess.

(Objective-C is in a similar place, but it doesn't aspire to have a clean, overloadable syntax.)

By contrast, C++ operator() is pretty handy and folks have built a lot of useful things from it.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading.

12 Likes