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

This is a huge improvement over the previous version. I’m a little nervous about having nothing special whatsoever to mark the unusual behavior—will people declare call() methods accidentally or fail to realize their significance when reading code?—but I’d be happy to see this merged unchanged.

1 Like

Like the previous review for this, I'm very enthusiastic for static callables. To me, this is something that has to be in the language given that we already have dynamic callables. It would be a big hole to only have support for one and not the other.

That being said, I don't really like that func call is the only thing that opts you in to this behavior. I realize that using func call drastically simplifies the required implementation, however, I don't think it would be too much of a lift to also require some marking annotation on something. Now, I'm not too picky on what gets annotated, but I would strongly recommend it be on the type.

While marking individual methods as @callable might be interesting, I feel it's important to pair this feature with how other features that use "hidden" requirements are implemented, namely with a marking annotation on the type. This allows:

  1. Allowing callable behavior to be an opt-in choice
  2. It has precedence with @dynamicCallable, @dynamicMemberLookup and the proposed @propertyDelegate feature.

I would say that the argument against type level annotations could've been applied to all existing @ annotations that give special behavior to a type. The reason given is

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 also an unfortunate edge case that must be explicitly handled: if a @staticCallable type defines no call-syntax delegate methods, an error must be produced.

Now, I would argue that it is extremely meaningful. It specifies that a type is meant to be operated on in a special way, and so deserves a special marker. And that "unfortunate edge-case" isn't an edge case at all, if a user specifies a type should be callable, and they're not specifying a func call or func _ or whatever the special method is named, then yes it should error!

As for adding callable behavior via extension, I disagree that this should be supported. Just like you can't add @dynamicCallable via extensions, why should we allow adding static callables via extensions? In my view, this is a feature that should be used only with certain types that were designed to be used with call syntax. Just because there are existing APIs today that could benefit from doesn't mean we should let anyone tack callable behavior on types they don't own. If the author of a type thinks it should be worked with in a callable way, let them do that.

5 Likes

Does that even matter, though? They can continue to use call(...) as a normal method if they wish, just like you can refer to .init(...) explicitly.

I'm a little surprised about the criticism here. This is how operators work in Swift - they are basically functions with magic names which require different syntax. For example:

struct A {
  let val: Int
  static func + (lhs: A, rhs: Int) -> A { return A(val: lhs.val + rhs) }
}

func a(inst: A) {
  // if "+" was a normal method, you would expect to call it like this:
  let n = A.+(lhs: inst, rhs: 42)
  // but that doesn't actually typecheck (on my machine, at least).
  // But both of these incantations work:
  let n2 = inst + 42
  let n3 = [1, 2].reduce(inst, +)
}

The magic name "+" is defined in the standard library as an operator, but it's not discoverable at all. You basically have to know that it already has this funky behaviour. I didn't opt-in to this weird syntax with an attribute or protocol conformance - all I did was add a method! What if I really wanted to write A.+(...) and not use the infix operator syntax? Tough luck! Swift has reserved that name.

I don't see much difference with the magic name call(...). True, it's a little bit further removed from the function-call operator (), but the intended behaviour is just as obvious as + IMO.

Alternatively, we could make it more directly mirror the function-call syntax and expand what we consider an "operator" to be. That was basically my feedback during the last round of review. Perhaps I didn't express it very clearly.

EDIT: wrong link. There were a couple of posts on the topic.

3 Likes

But func + itself stands out because the name is clearly special. It doesn’t need any additional marker to tell you something odd is going on. func call does not stand out in this way—it just looks like you’re declaring an ordinary method with the name call.

You are right, though, that misinterpreting it as just an ordinary method called call is not the end of the world. I chose what I said carefully: I’m “a little nervous about” this. I’m not dead set against it or convinced it’s a disaster in the making. But I also want to make it clear that, if others have doubts about this part of the design, I don’t think they’re wrong.

7 Likes

I agree that it might be a problem that there is no indication that this function name is special. Is there any precedence in the language for something like that?

I dislike that it "is something you have to just know" when you read the code. That's "unswifty" in my opinion.

init and deinit are clearly different, and as such I would possibly prefer having call also spelled without the func.

4 Likes

According to the Core Team:

On the first two points, the core team debated several designs and came to recommend that we keep callable syntax directly aligned with func to keep the function grammar consistent and make naming clear. In particular, 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. This would give us syntax like func call() . With this approach, you can invoke the callable with with either value() or value.call() , and can partially apply the callable with value.call .

I'm not sure how func call() is better than call() in terms of grammatical consistency.

Just like init() can be invoked with either Foo() or Foo.init() and can partially apply the callable with Foo.init, I wouldn't be surprised if call() can be invoked with either value() or value.call(), and can partially apply the callable with value.call.

And just like init() and subscript(), it'll naturally stand out.

8 Likes
  • What is your evaluation of the proposal?
  • 0.5 , suggest another revision
  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. static callable have better type safety than dynamic callable.

  • Does this proposal fit well with the feel and direction of Swift?
    Not in it's current form.

This creates yet another way to add capability to a type. Currently we already have 3 ways.

  1. With protocol, like Comparable, ExpressibleByStringLiteral
  2. With special keyword, like subscript
  3. With @ prefixed keyword, like @dynamicCallable, @dynamicMemberLookup

This is not one of above. Introducing a new and more implicit way to add capability to type make the type system less uniform.

We should stick to a @ prefixed one like @callable since core team doesn't like 1 & 2.

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

Python has it. but the hidden name __call__ clearly marked this as an advanced feature.

3 Likes

Maybe core team treat it as a operator overloading case instead of adding capability of type?
If that's the case, how about

struct Adder {
    let base:Int
    func ()(i:Int) -> Int {
        return base+i
    }
}

this is a parser error in Swift 5. It matches how Swift & C++ works with operator overloading.

3 Likes

I don't agree that the magic behaviour of methods named + is obvious - as I showed, when you do that, you completely lose the ability to call it as a method (i.e. with MyType.+(lhs: ..., rhs: ...)), and the infix operator syntax is the only way to call it. That's not obvious behaviour. At least this proposal allows you to refer to the call method directly.

Also consider that this behaviour is not limited to +, but is the general behaviour for all operators. Including user-defined operators.

I agree - func () (...) is the name I would prefer. That was my initial suggestion last time, if you want to read the feedback.

I also agree with your review that Swift has a grab-bag of magic syntax features which has become difficult to reason about and teach. For that reason I think it's important that we keep dynamicCallable in mind when discussing this feature, and I basically see two possible ways forward:

  • Either we model this feature on dynamicCallable, and take func call(...) to parallel func dynamicallyCall(args/kwArgs), or
  • We remodel both on them on operators. func () (...) for static callable, and maybe some pseudo-operator like func (*) (args/kwArgs) for dynamic callable.

In any case, I think we can get rid of the type-level attribute. It's extra syntax, and an extra hurdle that I don't think we really need.

5 Likes
  • What is your evaluation of the proposal?
    The proposal address the issue but I'd rather to use invoke instead of call
  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes completly.
  • 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?
    This proposal is similar to feature you can find in Scala and Kotlin.
  • How much effort did you put into your review?
    In depth review.

+1. I preferred the first-class syntax from the previous version of the proposal but I am OK with re-using a call function for the feature. In this case I think it would be better to require an explicit opt-in akin to @dynamicCallable (@callable?):

@dynamicCallable
struct DynamicCallable {
  func dynamicallyCall(withArguments args: [Int]) -> Int { args.count }
}

@callable
struct Callable {
  func call(_ value: Int) -> Int { value * 2 }
}

In the project I'm working on right now I just came across another use case to support the alternative of using a member attribute. In this case, the type has a property of function type that I would like to be callable:

struct Canceler {
    /// - important: Must be idempotent and thread safe
    @callable let cancel: () -> Void
}

Of course it is possible in the proposed design to write the forwarding wrapper. But it would be much nicer to not need to do that and to not need to have a call member if we don't want one. As I mentioned earlier, the attribute could be parameterized with the correct number of argument labels when it is necessary to provide different caller syntax.

The primary downside to this approach from a programmer perspective (I know it's a lot more invasive implementation) is that there wouldn't be a standard method name that would support abstracting over all callable types with the correct signature. This downside would be eliminated with a future direction of first-class callable constraints using function signature syntax.

I have similar reservations to others expressed here.

Magically having what looks like a normal function trigger this behavior is problematic for people learning the language or inspecting the code.

As a new user when I see special syntax like subscript(index :Int) -> Value? I have immediately added a new concept to my knowledge base. I may not know what it does or how to use it, but I know a thing called "subscript" exists. I can search for "swift subscript" and probably find something about it in the language guide or online. I can search a codebase for other instances of it to understand what it does. Seeing this thing that doesn't look like a function or a property helps reinforce the concepts. The same things apply to attributes. If I see @dynamicCallable I know something is going on here. I can search for that term.

A bare function that looks like any other function but happens to trigger new behavior for values of that type is the opposite. We've worked to eliminate other instances of magical implicit behavior (bridging, tuple splat, etc) and for good reason. We shouldn't introduce a new one.

Some languages like C++ have a call operator. We could certainly implement it that way:
func () (x: Int) -> Int { ... }.

But of all the possible options I think taking a lesson from subscript might make the most sense:
call(x: Int) -> Int { ... } (or substitute invoke/apply/etc)

13 Likes

My preferred spelling would still pretty strongly be:

func _ (a:Int) -> String
//or
func (a: Int) -> String

This has several advantages:

  • It is currently invalid code, so it won't break anything
  • It is special enough not to be used/created accidentally
  • "A function without a name is called this way" is very easy to explain
  • It is an elegant notation (as explained below)

One of the rules I have found useful for notation over the years is to think about "completeness", or how existing elements would compose in ways which aren't currently supported. For example, Swift uses _ to explicitly replace a name with nothing. We are saying we haven't forgotten it, but we don't want to put anything there. Thus we should look at all the places where we have names, and ask: "What would happen if this didn't have a name?". In the case of a function name, we need to ask: "How would this be called?". Calling it directly off the value is one of the best answers to that. If there is one fairly obvious answer to what a particular composition would mean, then I consider it an elegant notation because you aren't really adding concepts to the language, so much as you are adding a capability that falls out of the composition of current concepts.

(Side note: Another interesting place to look at the composition of _ is in generic syntax. If I have MyType<A,B>, what would it mean to say MyType<A,_>?)

For referring to the value as a function, I would like to see an explicit cast:

let storedFunc = myCallable as (Int) -> String

You are going to need/want this capability anyway for the case where you have multiple "call" functions, and it feels extremely swifty to me. Again it is elegant based on my definition above. It falls out of our current syntax naturally.

That said, under the review guidelines set forth by the core team, I suppose I have to say I want the feature in the language (even if it is a little awkward to use). +1 with the reservations mentioned above.

6 Likes

I much rather reserve this for anonymous closure syntax à la golang

1 Like

But we already have a syntax for anonymous closures. Am I missing something?

Come to think of it. There is no reason why not do both. Actually I am fine with both your suggestions.

The problem with this approach is that it is not simpler to understand or more teachable than the alternatives. It would still mean:

  • subscript has its own kind of decl
  • static callable will also have its own kind of decl
  • dynamic callable uses a function with magic name & type-level attribute
  • other operators (e.g. +) willl also use a function with a magic name, but no type-level attribute

There are also questions about scalability and tooling when introducing new kinds of decls, as discussed in the previous review.

If we introduced a new way to override member lookup, for instance, what’s the rule that helps us decide whether it should be a new kind of decl, or a function with a magic name (with/without attribute)?

I wonder if we should just introduce “operator” members, basically like C++. That would make the special behaviour/syntax absolutely explicit.

This proposal has been accepted with modification. Thank you to everyone who participated in this review!

-Chris

3 Likes