SE-0253: Static callables

I'd like to add my weight to @brentdax's questions: I'm surprised this proposal doesn't reuse a function declaration called call instead of introducing a new declaration.

1 Like

+1

Yes.

Mostly yes.

Regarding the syntax, what about making it func call(...) { ... }? This limits the scope of the change to call site sugar and it is purely additive so shouldn't be source breaking. The only issue is if this happens unintentionally.

Otherwise, both the proposal authors and @brentdax make strong arguments. I can live with either approach.

For beginners, I worry that being able to call something yet not being able to pass it as an argument to map and filter may be confusing. If implicit conversion will only come later, we should at least add a fix-it suggestion to add .call. This does expose an advanced feature that they may not be aware of, if for example they are using a library that is returning callables. Same thing with function composition, etc. All need to be modified by adding .call.

A quick reading.

  • What is your evaluation of the proposal?

I'm generally positive. It can drastically simplify code in some domains where the concept of a "function" is more than just a Swift function.

I have one nitpick though, which is expressed by several other people in this discussion: callable values should be implicitly convertible to function type.

struct Adder { /* contents omitted for brevity */ }
let adder = Adder(base: 5)
let add = adder as (Int) -> Int

The conversion should also happen if the type checker doesn't find a solution that takes the type directly but does find one that takes a compatible function type instead.

[1, 2, 3].map(adder)    // [6, 7, 8]

I immediately see this as a preferred implementation method for the (as of writing) ongoing Key Path Literal function expressions proposal, which itself lists callables as a future direction.

Implicit conversion would likely affect type checker performance. If this reveals itself to be a problem, I'd settle with explicit conversions only. I don't think it's likely though.

[1, 2, 3].map(adder as (Int) -> Int)    // [6, 7, 8]

As part of this, I'd remove the .call syntax. To reference the call member, the as operator is a sufficient and clear mechanism to do so, even for inexperienced users. We also avoid the special-casing rule where we disallow call members and func call members with equal type signatures.

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

It's a relative minor enhancement to the language that doesn't drastically change semantics. It's a relatively low-cost addition to the language.

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

Swift provides many syntactical constructs that not only increase expressiveness but, with careful API design guidelines, increase clarity. For the applications given in the proposal, I feel this feature can increase clarity in those domains.

There's opportunity for (notation) abuse but we could say the same about computed properties or subscripts that aren't used for their intended purposes. The benefits for correct applications outweigh the confusion caused by potential abuses.

Except for a potential use by key paths (see above), I don't see the standard library widely using this feature in its types. A novice user doesn't need to know this feature right away so it fits in Swift's progressive disclosure philosophy.

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

I have not used Python's __call__ or C++'s operator() but I'm familiar with those features. This proposal pretty much ports those in Swift. I haven't heard much criticism from Python and C++ folks for callables, beyond the concerns for notation abuse.

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

I've read the proposal a few times and I've followed the recurring pre-pitch discussions over the past few years.

I like the feature. Like Brent, though, I'm worried about the use of a new declaration kind, both for the massive impact on tooling (many parts of the compiler and SourceKit and many tools built on top of the compiler and SourceKit need to be updated), and for the increase in language complexity. It's not unreasonable to say this is syntactically and semantically distinct from normal methods and therefore it deserves its own declaration kind, but like operators this is something that can be modeled as a method and does not have significantly different rules from a normal method. The one restriction I see is the ban on static calling, which I do think makes sense.


Can call declarations be mutating? I think that should be permitted, though I also think it would be perfectly reasonable not to support that in the first version. (Looking very far ahead, a consuming call would provide the API of Rust's FnOnce, a callback that must be called exactly once on all paths. Getting all the benefits of that would require additional compiler support when forming a closure, though.)


The ABI stability and ABI resilience sections need to be filled out; this is hardly just syntactic sugar. For ABI stability, can programs that use call declarations be deployed to Swift runtimes that didn't support call (macOS 10.14.4)? Will that runtime have trouble with types declared inside a call declaration because it uses a mangling that it doesn't recognize? And so on. I don't actually know the answers to these questions, but we should before the proposal gets merged.

(For resilience, the questions are more like "Can I add a call declaration to a type that didn't have one? What about an overload?", and the answers should be the same as for methods. But that should be in the proposal.)

11 Likes

This is a great addition. Regarding implicit conversions, they are trivial sugar on the outside and a complex matter on the inside; I think they deserve a separate discussion. For now, we can use partial application, and it's only just an additional .call member reference.


Brent and Jordan have pointed out some reasonable arguments against adding a new contextual keyword, but I am slightly concerned about resilience. We probably want to be able to retroactively make types callable, do we? If the grammar is to denote a type declaration with some @callable attribute and reuse func call, what will the grammar to make an imported type callable be? Adding the attribute to an extension seems a bit odd.

Unlike operator, call is a perfectly valid identifier. Honestly, I don't like the idea that some functions may behave differently only based on their name.
This is how Kotlin works when using Java classes and it may result in methods that are interpreted as operators while they are not intended to in the first place.

IHMO, if we choose the func call() path, static call support should remain an opt-in behaviour.

  • What is your evaluation of the proposal?
    +1

  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes. Most people assume that swift’s most fundamental thing is a function because the can be declared in top scope, but in swift functions are non nominal types.https://stackoverflow.com/a/44396546/3705470

This proposal allows to get closer to a world where all functions are nominal types?

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

Yes but the name. It may sound perfectly fine to some to have a ‘call’ but to my untrained ear this sounds just odd. func function() please. function is more meta plus static ooperators already use func.

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

Na

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

Read proposal.

I don't know that we do. We don't allow @dynamicCallable to be applied to an extension.

(The logic here would be that you should only use @callable if a type is functionlike by its nature; it's not something you should retroactively add to a type as a convenience.)

5 Likes

What is your evaluation of the proposal?
I can appreciate the need to make types callable as functions. I think the addition of the feature is well-motivated and would apply to UI apps just as much as machine-learning modelling. I have often declared methods on types like generator.result(from: input) when generator(input) would have sufficed, the additional words don't really add anything but to satisfy the rules of the Swift language.

However, I think the proposal makes the wrong design decision on how to realise this feature. The call syntax is invented for very little purpose. It's not clear what justifies making this wholly separate type of declaration ... especially as it looks almost the same as declaring a method named call, the only difference being the lack of the func keyword.

The proposal tries to show a corollary to how subscripts are declared. I don't find the comparison to hold much weight as subscripts do signal special behaviour; they can contain set and get bodies and so forth.

Dynamic callables use a special method name and an attribute. I think this proposal should follow the same pattern, introduce an @callable attribute that looks for a func call(...) on the same type. This is also syntax backwards compatible as any existing types that have declared methods named call would not be affected until explicitly annotated with the @callable attribute on the type.

I also agree with @brentdax that the attribute illustrates the intent that "a type is function-like by its nature" and retroactive conformance does not make sense.

Is the problem being addressed significant enough to warrant a change to Swift?
Yes, but not enough to warrant the addition of an entirely separate class of declaration. An attribute-based design would not have that same turbulence.

Does this proposal fit well with the feel and direction of Swift?
Yes. Swift enables elegance and concision when behaviour is well-defined and unambiguous. This feature is exactly that and provides compile-time static checking. There are no 'dangers' here.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
Used Python heavily, they use a __call__ magic method name to declare a callable. It was useful. Adding this feature to Swift would offer the same utility and take advantage of compile-time guarantees that Swift can provide.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Followed the pitch thread, read through the proposal.

3 Likes

I think this is a good addition to the language, but not using a call keyword.

The syntax should allow you to use the call as a regular function type, which means starting with a named function with the func keyword, and using a @callable attribute:

extension Polynomial {
    @callable func evaluate(_ input: Float) -> Float {
        ...
    }
}

When referenced as a regular function, you can say let myFunc = Polynomial.evaluate as you would with any other function. And the @callable attribute lets you also use a callable syntax, polynomial(2).

I think this gives us the best of both worlds, and is in line with Swift's progressive disclosure philosophy.

2 Likes

If we do change the syntax from what is in the proposal I like this direction much better than an @callable attribute at the type level combined with call methods. call will often not be the best name for the method given the domain. For example, it might be interesting to make a value representing a monoid callable and the natural name for the method is combine, not call.

This syntax also works better with static static callables (i.e. callable metatypes) than the type-level attribute. The type-level attribute is the only option that would require additional syntax to support static callable. The attribute might be @staticCallable, in which case we would have a confusing mix of @callable, @dynamicCallable and @staticCallable. I suspect this would not be desirable and result in metatypes just not becoming callable.

One advantage of this approach is that in cases where we want to support both callable syntax and method syntax (combine in the preceding example) we do not need to implement both and forward one to the other. @callable func combine is sufficient to make both available. The downside is that it requires one to name the method and make it visible which may be undesirable in some cases. Supporting @callable func _ would probably be the best way to support opting out of making the method directly available.

4 Likes

I think this proposal needs to go back for further thought rather than being accepted. I am concerned about accepting it for two reasons: it has a poor benefit to complexity ratio, and it should consider better integration with the existing functionality of closures.

We've found ourselves considering a lot of sugar recently. Sugar is good! It makes the language nicer to use and is an important part of a balanced diet. But not all sugar is the same. Allowing key path expressions to be used as functions is a simple concept that seems unlikely to ever cause serious confusion, so is only a light dusting of cosmetic sugar. Return elision is not just sugar, it also makes the language more consistent by applying the same elision already allowed with closures. Property delegates are more heavyweight sugar, but that sugar is an important ingredient that will enable open up lots of new usage patterns.

By contrast, this proposal introduces a reasonably significant language change, a new keyword, and a new term "callables" into our glossary, all for a really trivial payoff. It's nice to not have to write the evaluate in polynomial.evaluate(2), for sure, but it's not exactly game changing. Unlike SE-0243, another-nice-to-have, I don't think it's nearly as obvious at the point of use what is happening. Others have mentioned a pretty big list of "how will this work?" unknowns that I won't repeat but that do worry me.

Finally, I'm concerned that not enough consideration has gone into how this feature relates to closures. A proposal for callable should properly address closures being callables – to fully flesh out what this idea would mean and how it would work, what benefits it could bring, rather than just mention it as an aside in the proposal.

17 Likes

Why don’t we use func by itself like golang anonymous closures. Is it a compiler limitation?

struct Adder {
    var base: Int
    func (_ x: Int) -> Int {
        return base + x
    }
}

let adder = Adder(base: 1)

print(adder(1)) //2
let myAdderClosure = adder.func
let myAdder1: (_ x: Int) -> Int = adder
let myAdder2 = adder as (_ x: Int) -> Int
1 Like

Thank you to @brentdax, @jrose, and others for pointing out missing pieces in the proposal: undocumented interactions of call declarations with other language features, and impact on ABI resilience/stability. I've started apple/swift-evolution#1014 to clarify some of these points.


It is the case that adding a new call declaration kind has a higher implementation cost relative to alternative syntaxes for call-syntax delegate methods.

But I strongly feel that we should evaluate "the best syntax for call-syntax delegate methods" at face-value: focusing on which syntax best fits into Swift and temporarily putting aside implementation costs. In particular, I feel more attention should be given to spelling and Swift naming guidelines.

Here is my opinion on some alternate syntaxes, assuming the following goals:

  • We want to support a call-syntax sugar: foo(...).
  • We want to support direct references to call-syntax delegate methods to avoid the necessity of thunks like { foo($0) }.
    • Implicit/explicit conversion of "values whose type define call-syntax delegate methods to function-typed values" works around this problem, but I would deem it out of scope for this initial proposal.

  1. Use a type attribute to mark types with call-syntax delegate methods.

    @staticCallable // alternative name `@callable`; similar to `@dynamicCallable`
    struct Adder {
        var base: Int
        // Informal rule: all methods with a particular name (e.g. `func call`)
        // are deemed call-syntax delegate methods.
        //
        // This approach is most similar to `@dynamicCallable`.
        func call(_ x: Int) -> Int {
            return base + x
        }
    }
    

    As stated in the proposal:

    We feel this approach is not ideal because:

    • A marker type attribute is not particularly meaningful. The call-syntax delegate methods of a type are what make values of that type callable - a type attribute means nothing by itself. There's an unfortunate edge case that must be explicitly handled: if a @staticCallable type defines no call-syntax delegate methods, an error must be produced.
    • The name for call-syntax delegate methods (e.g. func call ) is not first-class in the language, while their call site syntax is.
  2. Named call-syntax delegate methods: @callable func <#name#>(...).

    extension Polynomial {
        @callable func evaluate(_ input: Float) -> Float {
            ...
        }
    }
    

    This approach enables call-syntax delegate methods to have a (semantically accurate) name.

    I think @callable is a bit dubious because one could say "all methods (and functions) are callable". A more appropriate attribute name might be @callDelegate.

    A more serious problem is: naming call-syntax delegate methods becomes very tricky. Consider this example:

    struct MachineLearningModel {
        var weight: Float
        var bias: Float
        // Since the method is non-mutating, it should be named `applied(to:)`,
        // not the imperative `apply`.
        @callable func applied(to input: Float) -> Float { ... }
    }
    
    let model = MachineLearningModel(...)
    let input = ...
    
    // Direct method references now have proper naming.
    model.applied(to: input)
    // But call-syntax has an awkward label! This is killer.
    model(to: input)
    
    // The name `func applied(_ input: Vector<Float>)` fixes the call-syntax,
    // but makes direct references un-Swifty:
    model.applied(input)
    
    // `monoid.combine(_:)` violates Swift naming guidelines.
    // Same problem would apply to `monoid.combined(with:)`.
    

    AFAICT, this naming difficulty exists for any approach involving call-syntax delegate methods with custom names.

  3. Unnamed call-syntax delegate methods: func _(...), func(...), call func(...)

    This approach avoids the aforementioned naming difficulty, but leaves open the question of "how to directly reference call-syntax delegate methods".

    To me, func _(...) and func(...) do not clearly indicate "this is a call-syntax delegate method".

    call func(...) is clearer, and direct references could be done via foo.call. This is my current favorite alternative syntax, after call declarations. It requires support for unnamed functions (or at least unnamed methods).

5 Likes

An additional comment: I would like call-syntax delegate methods to be more first-class in Swift, like subscript and init declarations, rather than @dynamicCallable and func dynamicallyCall (type attribute and informal name-based requirements).

Subscripts have existed for a long time in Swift. Perhaps this is not an apt comparison, but imagine subscripts didn’t exist, and there’s a new proposal to introduce them. I would support a subscript declaration over alternatives like @subscript func(...) because it's more natural.

Similarly, I feel call declarations also fit naturally in Swift. With time, call declarations could be extended with more first-class functionality and further integrated into the language:

6 Likes

Could you please clarify what you would like to see in the proposal, to the effect of "properly address[ing] closures being callables"?

Would you like a detailed exploration of "unifying function types with nominal types" using call declarations (perhaps as Function protocols based on arity)?

There is an example of bound closures represented as a nominal type with a call member, and we could extend that exploration. I imagine the actual unification is out-of-scope for this proposal.

Ben, I don't understand your concern about closures. Closures are not special in any way w.r.t. this proposal, they are just a thing with function type, just like functions, partially applied methods, etc.

Also, it seems that there is a ton of confusion on this thread - without responding point by point, would people understand and feel more comfortable about this if the operator was spelled with a decl modifier as:

call func() {... }

This eliminates the declaration side grammar issues (because declaration modifiers "just work" in the grammar, without requiring a keyword), makes it clear that these can appear anywhere an instance func is, etc.

-Chris

3 Likes
// Direct method references now have proper naming. 
model.applied(to: input) 
// But call-syntax has an awkward label! This is killer. 
model(to: input) 
// The name `func applied(_ input: Vector<Float>)` fixes the call-syntax, 
// but makes direct references un-Swifty: model.applied(input)

This seems pretty minor and solvable. How about a callable automatically removes the parameter name?

@callable func applied(to input: Float) -> Float { ... }

The compiler gives you:
model.applied(to: input)
model(input)

What if you actually want argument labels for the call syntax?

at that point, just use the regular method.