SE-0253: Callable values of user-defined nominal types

I'm confused, is this behavior now implicit? Meaning you don't opt in with some annotation on top of implementing call?

EDIT: on closer inspection, yes.

I think that it is a little bit unfortunate that we don't opt in for methods that we want considered for the syntax.

+1 besides/despite that.

1 Like

One additional point, since this is a second review of this proposal, it is important and useful to take into consideration the core team's feedback on the first round. Here are some of the salient points taken from that post (please see the full post for more context):

  • The core team is very positive about the idea of this feature,
  • The core team doesn't think that a type level attribute is necessary, and there is no reason to limit this to primal type declarations
  • The core team does not want to support user-defined unique names for callable declarations
  • The core team recommends picking a standard name (like call or invoke ) that can be used as a normal named member, and which is automatically callable with function invocation syntax.
  • The core team discussed the fact that no matter what identifier is chosen, that this could impact some existing code that already uses that name. The core team doesn't find this concerning.
  • The core team would like discussion of various possible names to see if call is the best name, or if something else would be better.

-Chris

1 Like

The syntax looks really lightweight and easy to use! (If only more cool features were like that :slightly_smiling_face:)

EDIT: @michelf's func self idea below looks even better that func call (if we can resolve any overloading issues), since self is reserved already and it's instantly apparent that there might be special handling.

Personally, I preferred the previous iteration's call() { syntax (rather than func call() {), but I do recognize that there is a benefit to being able to actually access the call function as a bound closure. Unless I am mistaken, this would not be possible otherwise, as decls like call() and subscript() cannot be accessed as closures.

I think that if there was a way to access such declarations, a call() spelling would be preferable, since it seems less "magic" than a function called call or something else implicitly making an instance callable.

3 Likes

Will this feature also work with trailing closures? I couldn't find any examples in the implementation tests.

e.g. callable { _ in }
e.g. callable(42) { _ in }

It might be useful to hide the call method from code completion. An attribute has previously been suggested, for hiding APIs such as the ExpressibleBy...Literal initializers.

3 Likes

What is your evaluation of the proposal?

+1. Happy to see this uses the func keyword.

Questions:
What does the diagnostic say when somebody declares a static public func call(...)? I would assume that we are not going to disallow this from being declared but it would not participate on the magic forwarding.

Do properties participate on the callable magic?

struct Callable {
    func call(_ input: Int)->Int {return input}
    let call = {(_ input: Int) in return input + 1} // does this participate on the magic?
    static func call(_ input: Int)->Int {return input + 2} // Any warning here?
}

let calling = Callable()
print(calling(1)) // property?
print(calling.call(1)) // returns 2 so the property gets picked.

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

We already have dynamic callables, I think filling the static callable hole is a welcome change.

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

yes.

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

python and javascript

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

follower of the previous review.

1 Like

Can someone help me understand why this isn’t sufficiently convenient?

Using the headline example:

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

let add3 = Adder(base: 3).call
add3(10)

Obviously this becomes less convenient as the number of call methods increases past one, but storing those methods in yet another variable just seems “right” for clarity anyways.

Maybe I’m just missing something here? Are there drawbacks to storing the function in a variable that would be avoided by the proposal?

You can't easily get at the 'state' of an Adder, in your example to implement–for instance–memoization with the ability to flush cached values under memory pressure.

Right, but the solution to that would be trivial:

let adder = Adder(base: 3)
let add3 = adder.call
add3(10)

// flush `adder` later

? how… eliding it in your responds doesn't make it trivial. You can pass add3 along without adder and then doing what I said becomes dificult.

Ah, I see what your case was. I wasn't considering the case where add3 would be passed anywhere. But even then, if you had the intention of doing work on the Adder itself, why not just pass it directly then (you have adder)?

Sorry if I'm asking questions that have obvious answers — perhaps I'm not the target audience for this pitch as I can hardly see how this is necessary. It seems to me that the convenience being asked for can be accomplished today without any changes by means of storing the function in a variable.

I don't agree with the special syntax being based on funcs that happen to use the word call. If I were designing an API, special cases like this would be a definite code smell, and I believe the same here. A consistently designed solution would be either:

  1. A attribute like @callable applied to a function you want special syntax on, which would discard all param names
  2. All functions are also implicitly callable, so no keyword required
  3. An entirely different spelling, like func _, func(, or func self(.

func + call together is poor API design.

10 Likes

What happens to existing code that has methods with the "call" base name? Do they suddenly become functors? What happens if "call" is the name of a type-level and/or non-method member?

Is "call" a conditional keyword? The text doesn't read that way to me; it seems more like a magic identifier. Like @ebg, I don't think that's a good idea. We should go all the way and make this facility a first-class construct; make "call" a (conditional) keyword and declare them like "init".

@aemino has said something similar, but didn't think first-class call methods could be used as closures. Would that be accurate? We can use init calls as closures, for map and such, all the time, and a first-class call would have a structure more like init and not subscript.

6 Likes

It isn't clear from the proposal's wording whether static functions aren't eligible at all or whether they aren't eligible only when the parenthesis follows a type name. What happens in a case like this one?

struct Callable {
   static func call() {}
}

let c: Callable.Type = Callable.self
c() // equivalent to Callable.call() ?

In this case it isn't ambiguous with an initializer since you have to explicitly write c.init() to call the initializer. But I think for the same clarity reason you need an explicit init you should also be required to be explicit when calling the call function.

2 Likes

I think it’s pretty clear from the alternatives section that this is not currently supported. Personally, I think this should remain a future direction. I don’t like metatypes being restricted for arbitrary reasons. One example is that there may be good reasons to want metatypes to be able to meet callable constrains if / when those are implemented. In the meantime, I would think the compiler should probably warn for static func call since it won’t work the way people probably expect.

1 Like

I agree with the core team that this is an important feature. I don’t find the design entirely satisfying, but I do understand and appreciate the rationale laid out by the core team following the previous review. Under the constraint given for this review, I believe the proposed call function name is the best choice. I have used this name myself in cases where I really would have preferred the callable feature described by this proposal.

Overall response:

+0.5

I like the idea and the direction, but have specific feedback. I think that with some tweaks, this could be a nice addition to Swift, but I do not agree with its present form.


  • There are a few situations where this is a pretty “nice” feature to have. The Polynomial example given is a good one. Personally, I’ve come across others such as an Expression, a Filter, a Sorter, and a Mapper.

  • The BoundClosure example is a bit weird, mainly because the property is called value. I would expect the value of the closure to be what it evaluates to, not what the parameter is. It took me several readings to understand what this would really buy me.

  • I am glad that this is rejecting the idea of callable static members. Thank you for keeping initialization syntax mostly unambiguous

Specific Criticism:

I do not think this should be a method. In all of the cases, it is better from an API perspective to offer both an “evaluate” method (or calculate or apply or parse, as the document correctly identifies) and the call functionality. There are definitely times where contextual readability would prefer one over the other.

From an API-maintainability perspective then, it is unfortunate to have to implement bothevaluate” and call, and to implement one in terms of the other. This increases API surface area, which increases maintenance effort, and increases the likelihood of bugs or incomplete features. This is especially evident in the Adder example. The more complete and developer-friendly API of Adder, under the current proposal, would have that API be:

struct Adder {
    var base: Int
    
    func adding(_ value: Int) -> Int { 
        return base + value 
    }
    
    func adding(_ value: Float) -> Float { 
        return Float(base) + value 
    }
    
    func adding<T>(_ value: T, bang: Bool) -> T where T: BinaryInteger {
        if bang {
            return T(Int(exactly: x)! + base)
        } else {
            return T(Int(truncatingIfNeeded: x) + base)
        }
    }
    
    func call(_ value: Int) -> Int { 
        return adding(value)
    }
    
    func call(_ value: Float) -> Float { 
        return adding(value)
    }
    
    func call<T>(_ value: T, bang: Bool) -> T where T: BinaryInteger {
        return adding(value, bang: bang)
    }
}

If I, as the maintainer of Adder, decide to add a new adding method to support adding a new BigInt, then I have to remember to also add a corresponding call() method. This is needlessly verbose and is a very error-prone experience. Look at how many situations we have here in the forums where we have to go back and add in this little extension here or there because we added a feature in one place, but didn't account for all of the other situations where we'd want something similar. Directly callable methods will compound that problem.

Instead, it is far simpler to implement the functionality once, and then decorate the method with a @callable attribute. There's far less concern for incomplete API, as we only have to implement the functionality once and not have to also provide a shadow call() version.

Using a compiler attribute also eliminates any extra work needed to support a direct reference to the call method, because you would just use a bound instance method, like we already do:

let add1 = Adder(base: 1)
let f1: (Int) -> Int = add1.adding(_:)

I realize that the compiler attribute is discussed and dismissed under the "alternatives considered" section, for two reasons, which I'll reproduce here:

  1. First, we feel that using a @callableMethod method attribute is more noisy, as many callable values do not need a special name for its call-syntax delegate methods.

  2. Custom names often involve argument labels that form a phrase with the base name in order to be idiomatic. The grammaticality will be lost in the call syntax when the base name disappears.

The rationale for the first one is directly contradicted by discussion previously in the document, where pretty much every situation where this would be really useful has been shown to have a usefully "named" counterpart: evaluate, apply, parse, calculate, and so on. Proper and considerate API design would dictate that, as much as possible, an API should provide enough flexibility to allow clients to use it in their own, readable without without compromising maintainability. Thus, we should expect that all implementations of this feature should offer both versions: a named implementation and a callable implementation. If we accept that premise, then we accept making this feature be a call() method is asking API maintainers to duplicate their work.

The second reason is a little bit more interesting, but I also believe it to be a red herring. Like with @dynamicMemberLookup and @dynamicCallable, this is not a feature we expect many developers to be implementing haphazardly in codebases. Because this is a targeted feature, the "grammaticality" seems like a non-issue. If a situation truly arises where a @callable method is grammatically awkward, then a conscientious and considerate API producer would either choose to make the method not @callable, or expose a @callable version that is more grammatically correct.

On top of that, the contrived example of a callable Layer is so alien to the rest of the document that it seems entirely out-of-place. Of course it sounds awkward, because "applying a layer to an int" is itself completely non-sensical without a much larger context.

Of all the reasonable examples given in the document (expressions, polynomials, parsers, etc), it is easy to imagine what a method on those would look like that would be unambiguous. In many cases, the presence of the labels would increase readability.

The Polynomial example is a great one:

struct Polynomial {
    var coefficients: [Float]
    
    @callable
    func evaluate(at x: Float) -> Float { ... }
}

let curve = Polynomial(...)
let value = curve(at: 42)

Thus, I believe the dismissal of the @callable (or @callableMethod or whatever it is bikeshedded to be) is premature.

I do recognize that the core team expressed that they:

[do] not want to support user-defined unique names for callable declarations (e.g. allow call func myCallable(){}): this would complicate the internals of the compiler (including overload resolution) and thus the core team would like there to be a single (language defined) name.

To this, my only response is:

Swift exists for developers. Developers do not exist for Swift.

I believe that making this be a separate method from all existing functionality on a type is an overly burdensome requirement to place on all developers wanting to implement this feature that outweighs the complexity these additions would add to the internals of the compiler.

8 Likes

+1, with a small tweak:

Unify callable functionality with @dynamicCallable

Both @dynamicCallable and the proposed call methods involve syntactic sugar related to function applications. However, the rules of the sugar are different, making unification difficult.

They are different features with different signatures for sure, but the biggest head-scratcher is why dynamic-callability is implemented with a magic function name + type attribute, while static-callability would only require a magic function name, with no type-level attribute.

This proposal appears to indicate that the attribute isn't really needed, so I'd say we should also drop @dynamicCallable's attribute. I would make that change part of this proposal. IMO, unification is a desirable property. The two features are obviously very closely related.

Since we already have func dynamicallyCall(...), I suppose func call(...) is the obvious name. Collisions are no big deal since you can now refer to the call member directly: if your code said myObject.call(...) before, that should still work. The only difference is that you can also write myObject(...) directly.

I guess I'm a little hesitant because the word "call" is not literally the same as the "()" syntax; but at the same time, initialisers and dynamicCallable also have different names at the point of use. So whatever ¯\_(ツ)_/¯.

2 Likes

Hi all.

  • What is your evaluation of the proposal?

+1

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

Yes, sure. It is important direction to make Swift user friendly language for mathematical/data science domains. ML is very actual domain now with very strong tools competition. So any Swift evolution delays in this domains unacceptable.

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

Yes.

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

I think that similarity with Python __call__ is very important because we have Python interoperability now. It can simplify (along with other features) Swift integration into data science community.

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

Quick reading.

Thanks for such promising Swift evolution.

There's no other option, isn't it?
Afaics, that can't break any existing code, but still, I agree that it does not feel like a good fit for Swift to rely on spelling details rather than having something explicit (I guess it reminds me to much on the ARC-transition of Objective-C, where there was no other choice than relying on conventions).

I think no matter what method name is blessed, there will always be situations where people will have to watch out not to use it accidentally:

addressbookContact.call() // initiate a connection
addressbookContact() // ???
1 Like