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

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

When I said I am not entirely comfortable with the proposed design it is for the reasons @davedelong articulated about method naming. I limited my review to the scope the core team asked, but now that this line of reasoning has been posted I want to concur with it.

If the core team is willing to revisit the decision about impact on the internals of the compiler I think an @callable attribute is work considering further. The issue of argument labels is an important one, but it can be solved. Let’s consider the parser example from the proposal:

struct Parser<Output> {
    @callable
    func applied(to input: String) throws -> Output {
        // Using the stored state...
    }
}

let parser: Parser<Int> = ...
parser(to: “42”) // the label makes no sense without the base name

The argument label issue can be resolved by allowing @callable to specify argument labels used when the call syntax is used:

struct Parser<Output> {
    @callable(_:)
    func applied(to input: String) throws -> Output {
        // Using the stored state...
    }
}

let parser: Parser<Int> = ...
parser(“42”) // much better

The arguments in favor of the proposed design are consistency and ease of implementation. The argument in favor of the above design is that it allows us to express our intention more directly, avoids forwarding boilerplate when we want (or need in the case of protocol conformances) a different (more descriptive) alias to be available, and it avoids cluttering our type with a call member that we often may not really want to have directly available (but would tolerate if necessary to get the callable syntax).

The above approach to resolving the argument label issue is new since the previous review thread. I hope it will receive at least some consideration.

1 Like

Would a custom callable type support trailing closures? e.g.:

myThing {
    // this code passed as a trailing closure
}
1 Like

Are you asking in the context where the last argument is a function type? I would certainly expect them to support trailing closures in that context. But it would be good to have that stated explicitly.

Yes. Just thinking of DSL implications.

I think that the arguments around parameter labels are bogus to begin with. With the current proposal, we'd have to write that example as

struct Parser<Output> {
    func call(to input: String) throws -> Output {
        // Using the stored state...
    }
}

let p = Parser()
p(to: ...) // ???

The obvious thing to do would be to leave out the parameter name:

struct Parser<Output> {
    func call(_ input: String) throws -> Output {
        // Using the stored state...
    }
}

let p = Parser()
p(...) // much better

The same applies to the first example. If a method is going to be marked @callable, the developer should choose parameter labels that are appropriate for the more-commonly expected usage.

We don't expect to see p.call(to: ...), so why would the developer give the first parameter a required label? If both are desired, then a second method with a label can be added as an overload.

The point is that there is no most commonly expected usage syntax. The usage syntax that makes sense depends on the usage context. For example, there may be a Monoid protocol which requires a combine method, but the author of concrete monoid types may also desire for their concrete type to also be callable.

You can’t just discard argument labels altogether. There may be argument labels that are necessary to make the usage as a callable read clearly and be descriptive. If we don’t want to allow users to specify them in an @callable attribute then this approach simply isn’t viable and manual forwarding will be necessary.

1 Like

I think your suggestion is a good one. @callable(_:) nicely balances between allowing the developer to choose an appropriate function name while having a readable call site for either invocation.

My point is that argument labels is not a good reason to shy away from a developer-chosen name, because you can make .call(...) or instance(...) equally unreadable, depending on labels. The name alone does not allow for both styles to be readable, so there's no reason to force call.

1 Like

I think this defeats the purpose of using an attribute and it makes the api surface quite big. If an attribute were to be require, it should have no configuration. If the signature doesn’t match what it should be exposed as callable then another method should be created and tagged appropriately.

I don’t feel 100% about func call having such magic and I do think that a sigil like an attribute is probably needed but it should probably be something like func #call

1 Like

I'm going to add on with what everyone's been saying: func call with no other annotation is too subtle. I was in favor of that because it would greatly simplify the implementation and therefore future maintenance, but that was for the first version of the proposal, with an annotation on the type. If the motivation for dropping the annotation is "you don't need an annotation for the special syntax of subscript", then now it's important to note somewhere that you're opting into special syntax, and a separate declaration kind would make that clear.

(Last time I raised concerns about backwards-deployment with a new declaration kind. I think those can be resolved with something like "a call declaration is mangled like a plain function with a funky name call$$", but it would need more investigation.)

10 Likes

What is your evaluation of the proposal?

I fear call has too much potential of being used as a normal function name to assign it special semantics.

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

Is there a precedent of giving special semantic meaning to a regular function with a regular name? I ask because it doesn't feel right to me.

My preference would go to naming the function self, effectively meaning you want self to act as a function:

func self(_ x: Int) -> Int

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

D has a magic function name that does the same: opCall. Unlike call, opCall isn't a name you're likely to use without the intent of making your type function-like. Likewise for operator() in C++.

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

Quick reading of this one & participated in previous review thread.

4 Likes

What does value.self resolve to when using func self?

Eventually we may be eliminate the question by making the function type a supertype of Self, but in the meantime I'd propose resolving this to the function type where a function is expected, and Self otherwise, if this is acceptable and practical.

self would lose the ability to grab a bound method:

let functor = ...
let function = functor.call // OK: it's just a regular bound method
let function = functor.self // Weird

This is interesting, and preserves the support for bound method. D has opCall. Python has __call__. The game is to find a method name which is unlikely to be used for another purpose. Maybe Swift could have:

  • func opCall(...) (not very Swifty because of the "op" abbreviation)
  • func __call__(...) (not very Swifty because of underscores)
  • func functionCall(...)
  • func functionOperator(...)
  • func func(...)
  • func callOperator(...)
  • func callSelf(...)
  • func selfCall(...)
  • func asFunction(...)
  • func Call(...)
  • func CALL(...)

asFunction is not bad.

Call is interesting as well. User-provided attributes also start with an uppercase letter, in the property delegates proposal (@Lazy). Using the uppercase initial as a marker for user-provided magic may be a rich area.

I guess it'd have to work like this:

let function = functor as (Input) -> Result

If the functor can be coerced into its function type where needed, that makes it a subtype of the function type. And then you can write:

array.map(functor)

instead of:

array.map(functor.call)

I feel the .call version much less inspiring. It could be nice as a temporary hack while we wait for the coercion feature... except for the fact that nothing you add to the language can be temporary.

4 Likes

What is your evaluation of the proposal?
I'm very happy about this. With implicit conversion to functions this will be very powerful tool for concise and clean syntax. Also, I'm very happy that it became just a function name without any special syntax, as adding new keywords continuously is not good for language. The issue with different names addressed above (parse, evaluate, etc) will be fixed when implicit conversion lands – there will be no need to use call directly in any case.

Is the problem being addressed significant enough to warrant a change to Swift?
Sure, I've written a lot of ugly code like bazer.baz().

Does this proposal fit well with the feel and direction of Swift?
Yes, giving that Swift tries to be a multi-paradigm language. That will create nice connection between OO and functional style.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I've enjoyed it very much in Scala, especially with array(2) and dictionary("key"). Btw, Scala doesn't have subscript, and it is very fascinating to see that Array<T> is just a function - (Int) -> T and Dictionary<Key, Value> is just (Key) -> Value?. Imo, at first glance it seems just like a syntax sugar, but actually it blurs the boundaries between objects and functions, that changes the way you look at those things.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I've read both proposal versions.

PS. Giving the recent proposal for keypaths acting like functions, it would be very nice that keypaths would support \Foo.call, it would result in cleaner code like container.map(\.call) to create array/optional of functions. In my view, it will eliminate the requirement for implicit conversion to functions be supported in runtime – it is hard and, imo, isn't needed that often.

It would also complicate developer life, as you would have to scan the entire type looking for annotation each time you try to guess which method is used when using a callable type.

This will be even harder if the developer is allowed to use different method name for overloading.

2 Likes

I think the desire to be able to directly reference the call function is still causing a lot of trouble with this proposal. Trying to make the same argument labels read well at the use site with either variable.call(…) and variable(…), where variable itself is an arbitrary name can be difficult or impossible (e.g. polynomial.call(2) and model.call(x) from the proposal don't read well, which is why their unsugared versions were written as polynomial.evaluated(at: 2) and model.applied(to: x)). If you drop the requirement of being able to directly reference the call function then this makes alternatives more palatable and solves the naming issues, at the minor expense of having to use a workaround for direct references (either closures or forwarding methods).

My recommendation would be to use one of the unnamed alternatives from the proposal (func (…), func _(…), func self(…)) but I have no strong preference for which one.

3 Likes

I don't think it's necessary. The entire point of this feature is that developers won't use instance.call(...), but will only use instance(...). We can't control/force* such behavior, but I don't see why there's strong concern for how the call site will read when not using the intended approach.

We don't concern ourselves with the variable name picked to refer to a function:

let milk = Polynomial(4).evaluate
milk(at: 16)

But we all recognize that it's up to the caller to use the feature in a sensible way.

* i.e. we'd have to go with your approach to force it, but if we stick to the proposal, we have no mechanism.

4 Likes

Good question!

  1. It doesn't work for mutating functions on value types, where capturing a method would result in making a shared mutable reference, defeating the whole thread-safe shtick that relies on value types being unaliasable.

     struct Incrementer {
     	var i: Int
    
     	mutating func call() {
     		i += 1
     	}
     }
    
     let inc = Incrementer(i: 0).call
    
  2. The closure that's produced would be a separate heap-allocated (and ARCed) box, that isn't necessary. Granted, it would usually be inlined, but still.

  3. This pattern can "catch on" informally, but there would be fragmentation over the exact name of call. Some will call it invoke, some apply, and so on. Among other things, this proposal codifies this pattern into the language, and gives us common terminology to work with.

1 Like

Thanks for the reply! The point about capturing a mutating func is one that I hadn't considered.

I'm not so concerned about your third point as developers tend live on a spectrum of bad-to-good names. Fragmentation in naming will never be completely done away with (e.g. you can call your CodingKey whatever you want), but this approach does do a nice job at forcing the consistency of call by making the opt-in mechanism the name of the method itself.

All of that aside, I guess my approach doesn't hold much water once you poke it with mutating, which I thank you for because now I feel a little better about this pitch — so yeah, thanks!