SE-0253: Static callables

I feel this is more naturally expressed as an overload of the ()(call) operator. Then you start to wonder about @dynamicMemberLookup as an overload of the .(dot) operator. It's a bit disappointing that C++ appears to have a more coherent story here, whereas we have a grab-bag of attributes and magic requirements.

The main issue with making it an operator, IMO, would be that Swift operators are pretty awkward and written as static member functions with the instance as a parameter. Which, in turn, you really have to wonder about.

I mean, if nobody wants to write this:

extension MyThing {
  static func () (self: MyThing, arg: String) -> Int {
    /* .. */
  }
}

Then why do we make them do it for simple things like Equatable? Why can't we write something like:

extension MyThing: Equatable {
  func == (other: MyThing) -> Bool { 
    /* ... */
  }
}

or, for callables:

extension MyThing {
  func () (arg: String) -> Int {
    /* .. */
  }
}
2 Likes

Pure speculation: I don't think there are truly fundamental blockers to supporting static func (), but it would definitely require parser changes (and maybe hard complications).

The important question is: is func () is a good/desirable name for call-syntax delegate methods? () does match the syntax for function application, but I'm not convinced it's an ideal name in Swift.

A similar direction from the pitch thread is to name call-syntax delegate methods func self (or static func Self, the static is added by me in the code below):


Personally, I prefer call declarations and unnamed methods (e.g. call func _) over both func () and func self. I feel the latter are more confusing.

1 Like

self has an obvious drawback that it's already a valid identifier elsewhere in the object. I don't think Self can be used in quite the same way, but it'd still be quite confusing to read.

struct Adder {
    var base: Int
    func self(_ x: Int) -> Int { // instance method
        return base + x
    }
    func someOtherFunction() {
      var a = self // uh-oh: this 'Adder' or method reference (Int)->Int ?
    }
}

I know that func () isn't the best thing, but I still think we should be approaching this from an operator perspective. I thought this was exactly the kind of thing operators were for.

1 Like

I agree that there's room for exploration with operators, though custom operators and operator overloading are historically controversial topics.

Back on topic: I don't think func () is representable as a standard operator, because any arguments would separate the ( and ) characters and split up the operator:

f() // I suppose `()` (zero argument application) is like a postfix operator.
f(1, 2, 3) // But not if there are arguments: `(` and `)` are split up.

Since func () cannot be modeled as a standard (postfix/prefix/infix) operator, I don't think the spelling func () has any inherent advantages over other spellings for call-syntax delegate methods. They all require special case logic, to some degree.

Well what I'm thinking is that we have a couple of closely-related features, and tantalising gaps:

kind of like an operator ():

  • dynamic callable
  • static callable

kind of like an operator .:

  • dynamic member lookup
  • static member lookup? A forwarding mechanism like C++'s operator -> perhaps? (which, FWIW, also behaves a bit differently to other operators).

Of course they are kind of magic operators, because they are built-in to the language and can support multiple signatures and so on. Then again, the other solutions like a new decl type are also kind of magic features that don't generalise to other things in the language.

This is digressing a bit, but looking at the proposal for @dynamicMemberLookup, I see that marker-protocols and methods are listed under "alternatives considered", but I don't remember if we also considered operators.

Anyway that's kind of the extent of my thoughts on the subject, as something for the core team to consider. I would very much like the functionality, however it is spelled, and I think that implicit conversion to function types is important. I don't think we need a callable protocol - callable is already implied by being written as a function type, and we already do closure specialisation which one would hope could be generalised to handle any callable object.

2 Likes

The proposal has been updated:

  • Direct references to call members via foo.call is no longer supported.
    • For now, create a closure { foo(...) } instead. Explicit conversion via as is the future direction, implicit conversions need exploration.
    • This simplifies source compatibility since there's no need to handle call member references.
    • Implementation-wise: call declarations will be represented like "instance methods with a special case name", not "instance methods with the name 'call'".
  • Clarified ABI impact: call declarations will not be supported when deploying to a Swift 5.0 runtime.
    • We chose this as the most straightforward option.

Feedback is welcome! (I've yet to implement these updates, will do so when I get a chance.)

6 Likes

"Operator" means something specific in Swift today: a function with a specific name that takes either one value or two values as input at the call site with a particular spelling. Subscripts aren't "operators" in Swift even though they are in C++. I don't think operator () is particularly clear even in C++, since the parameters aren't even written inside the parantheses.

8 Likes

Just a minor point, which might not need stating: I think tacit in @Karl's idea is to expand the scope of "operators" to support blessed "operators" () and ..

It's a bit intentionally outside of the current definition of operators in Swift. And it's an alternative mechanism for creating first-class-y callable and subscriptable hooks. Otherwise, I totally agree that the naming and usage are highly unclear.

Yeah, I understand. My response is that doing so offers no benefit, since those features in the language don't behave like other operators anyway, and we don't get any syntactic convenience from having them declared like that.

First off, I am very busy these days and didn't get a chance to read the thread. I just glanced a few posts and did a super quick read of the proposal. I apologize for taking up bandwidth, if my opinions and proposed solution are already considered and rejected.

Huge +1 for static callable feature,
Huge -1 on the proposed design.

MHO:

This feature should be implemented as pure syntactic sugar and should not create any ABI or source compatibility issues.

It should not introduce a new declaration kind as proposed with call declaration.

It should preserve arbitrary name for the implicitly called functions and properly support labels for all arguments, including the first.

My proposed solution is this:

Use a compile-time attribute to transform any member function tagged with it to be used to make the instance "callable". For example (super contrived):

struct Multiplier {
    var factor: Double = 1.0
    @implicit func applied(to argument: Double) -> Double { return factor * argument }
}

Given:

let twice = Multiplier(factor: 2.0)

the above @implcit attribute, will transform the following statement (at the parse level):

var x = twice(6.0) 

into this:

var x = twice.applied(to: 6.0)

Here is some more detail:

We can support argument labels for the implicit call as a parameter to the attribute, as follows:

struct Multiplier {
    var factor: Double = 1.0
    @implicit(by:) func applied(to argument: Double) -> Double { return factor * argument }
}

Then we would call it as:

let twoMultiplied = Multiplier(factor: 2.0)
var x = twoMultiplied(by: 6.0)

My implementation idea is that we would generate always inlined functions with invalid names at this stage so that we can use the diagnostic for overload resolutions and collision detection:

@implicit func foo(x: Double, y: Double) -> Double { ...

Should generate foo(x:y:) as usual and also emit an always inlined forwarder function $call(_:y:). We can add/preserve label by adding a parameter to the attribute:

@implicit(x:) func foo(x: Double, y: Double) -> Double { ...

would generate $call(x:y:). Valid overloads for the callable will be exactly the same as $call functions.

This means the implementation would work in two steps:

  1. Generating these thunks
  2. Substituting instance(args) with instance.$call(args).

All $call instances will optimize away and we are left with normal function calls in the binary.

I don't have time to better flesh out my idea or improve the presentation. Hope this helps the discussion.

I believe so. I was one of the first people to mention callable feature for types and specifically ask for them many years ago.

Again I believe so. Although it is an advanced feature. That is why I oppose call declaration blocks. A call declaration type puts it right next to subscript, but it is a more advanced and specialized feature compared to subscript.

Yes. Don't like the proposal as it stands compared to other languages.

Super quick read

1 Like

The main disagreement in this thread seems to be about how first-class a call-syntax delegate's declaration syntax should be. Those who believe this feature deserves a more first-class notion prefer call(...), while others prefer an attribute. There are lots of very good reasons on either side.


In response to the general feedback up to this point, I think, in the worst case a @call attribute that accepts argument label reconfiguration like @hooman suggested can achieve what we need. When the function needs to be anonymous, _ can be used as the base name.

struct Polynomial {
    /// Represents the coefficients of the polynomial, starting from power zero.
    let coefficients: [Float]
    @call(_:) func evaluated(at input: Float) -> Float
}

let poly = Polynomial(coefficients: ...)
poly(2)
poly.evaluated(at: 2)

That said, I think this would be a really unfortunate direction: It's forcing the user to make a choice about whether a call-syntax delegate should be _ (anonymous), and sometimes to come up with two names (one for method call, one for call-syntax). I don't think this will look very nice under future directions of the type system such as inheritance from function types (highlighted below). As for argument label reconfiguration, I'd rather define it away than have to allow it in a weird way.


In contrast, we strongly believe in @Joe_Groff's idea/vision of allowing function types to be used as a conformance constraint. In the following example, Adder would be a subtype of (Int) -> Int and thus can be implicitly converted (Int) -> Int naturally.

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

We believe that the declaration of a function types' call-syntax delegate should be just as first-class as the function inheritance feature itself, and that the call(...) declaration is consistent with that vision. If the community and the core team would like to see a pitch from a different direction, i.e. inheritance from function types, we will be happy to write it out. Otherwise, we believe the proposal is well-scoped as is, and will be happy to add a section about function inheritance as a future direction.

7 Likes

Why is it worth a new keyword "call" when there are options (e.g. func _ ) that don't need a new keyword, but provide the same functionality/model?

I don't know that call is any more intuitive to anyone who isn't coming from Python (and who isn't also familiar with their __call__ syntax). It will most likely require the same amount of education to teach the feature.

2 Likes

Good point, I agree that education is important.
Personally, I evaluate designs by thinking about questions like:

  • "How nicely does the design fit into Swift today and Swift's future?"
  • "How would I feel about the design if I were a Swift beginner and exposed to the design for the first time?"

Ease-of-teaching is certainly an important metric, but to me it's not on the same level as the questions above.

1 Like

Let me rephrase: Given your questions above, how does adding a new keyword call help over other options like func _ () that do the same thing without a new keyword?

Short answer: let's use a first-class declaration for a first-class feature (callable functionality).

Restating @rxwei above, I see the call-syntax delegate proposal as the start of a movement to 1) make "callables" more first-class, and eventually 2) to redesign function types as nominal types using 1). (We are happy to flesh out this direction - please give us feedback.)

Let's say Function protocols (see Rust, Scala) are added to the standard library at some point. I would like them to have call(...) members, not @callDelegate func _(...) methods.


Nit-picking your mention of func _(...): "unnamed method" by itself does not clearly imply "call-syntax delegate method". I would additionally add some kind of method attribute, like @callDelegate, or @call(_:) shown by @rxwei.

Aren't both of these completely possible with func _ ()? What exactly do you mean by "first class"?

Is it that the declaration doesn't stand out enough from other types of functions? If so, why is that helpful? Things like subscript are named differently because they behave differently.

Doesn't the word func allow forward transfer from the expectations around writing other types of functions instead of having to lookup what makes call different? (This is why I supported @Chris_Lattner3's call func over just call.)

I am also not in favor of having an attribute. To me an attribute is for adding information about a declaration (but in general you should be able to read it without the attribute and still understand the gist of what is going on). It is like an adverb/adjective.

Right, but why should I have to know what a "call-syntax delegate method" is? We could just as easily call them "type-level functions" or something else. I don't think the word "call" has the same intuitive meaning to me as it does to you (I come from an ObjC background). I read it as a verb, but we use it as a noun. So I have created a thing called a "call" that is somehow different... it's just not intuitive to me. That said, people will just learn it as a magic word.

My point above was that both spellings will require roughly equal education for a user new to the feature (with some useful forward transfer if we have func in the declaration somewhere). I am totally willing to add the word if it is actually needed/helpful, but I don't see what we are buying for the price of burning a keyword.

1 Like

I think all discussion about syntax has never been about "what is possible", but what fits best with Swift and future Swift.

By "first-class", I mean something that is "integrated into the language". For example, attributes are less first-class, because they are annotations and not core. Declarations, by comparison, are core and first-class.


Regarding the central question of "first-class or not first-class": if you believe that callable functionality is not first-class, we may have to agree to disagree.

In this thread, we've heard people's thoughts on this matter. I'd like to hear even more people's thoughts.


To further clarify my view: callable functionality per se may not be first-class-worthy. But function types and function application are certainly first-class. And we can design them in terms of callable functionality, if we generalize callable functionality and make it first-class too.

1 Like

Oh no. I definitely believe that this feature should be integrated at a core-level of the language. I am excited for the idea of being able to abstract the idea of functions in the future. I'm just confused as to what value that the keyword is adding (vs other syntax which can equally be integrated at the core-level)...

As I said above, I don't think it should be an attribute.

I'd like the lightest syntax possible, and I would like forward transfer from things like func where possible... Both of those should increase adoption.

This is the central point we seem to be stuck on. How does the keyword fit better with Swift? How can it grow in ways that the other options can't or won't?

I'm not trying to be difficult, I feel like I am missing something about why the keyword is useful and I am trying to understand what specifically makes it work better in your mind.

I guess I am looking for something like where this new call word could potentially naturally compose with some future or existing syntax, and that is why we need it...

I would really like to see this direction fleshed out. It isn’t clear to me how it can be made to work when you consider all of the argument type modifiers such as inout, @escaping, etc. We don’t have a way to abstract over these the way we can abstract over argument and return types. If you have ideas around how to handle this I am very interested in knowing what they are.

Do you envision the “callable constraint” syntax (i.e. T: (Int) -> Int) discussed upthread to be syntactic sugar for these protocols? That would limit a type to conforming once to each protocol where callable itself allows any number of overloads. This obviously makes type checking easier and would probably handle most use cases but it is less general than introducing a callable constraint that is not backed by a protocol (which would recognize all overloads). The protocol approach also requires an explicit protocol conformance declaration instead of just recognizing the callable declaration itself.

3 Likes

I think making function types into nominal types is not a good goal for these reasons. Whatever regularity it may achieve would get drowned out by the complexity we need to add to accommodate the full expressivity of function types we have today. When I think of functions as generic constraints, I'm not thinking of them as sugar for some Function protocol, but as a special constraint kind of their own, that can also accommodate argument and return attributes. That's how Rust handles them (or, at least, has historically).

I also think that, whatever future directions we may chart out, we could integrate any of the syntaxes we come up for call operations here into them.