SE-0253: Static callables

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.

Thanks for your feedback, @anandabits and @Joe_Groff.
I'm sorry, point "2)" from me was uninformed. "Special function constraint kinds" sounds quite more sensible.

This answer is flippant and not at all convincing. Dynamic callables and dynamic member lookup are also first-class features.

That said, func _ isn’t a great alternative either. It doesn’t feel Swifty and isn’t a very natural way to reuse _. Both func call and @<someattr> func whatever feel more consistent with other type system special cases.

Taking a completely different approach to dynamic callables doesn’t mesh well with this goal.

1 Like

I feel your response is a bit strong. Dynamic callables (call-syntax sugar with special handling of argument labels) are a niche feature, and thus have niche syntax. Callables (call-syntax sugar) are less niche and deserve more first-class syntax.

Example from Scala:

I think dynamic callables and callables are orthogonal language features and making them mesh well is a non-goal. Making them share a mechanism for declaring call-syntax delegate methods is also not necessarily a goal.

3 Likes

Love @implicit, this is exactly what a call is here and should be applicable to any function.

2 Likes

The meaning of "implicit" is too broad for this feature. Function declarations marked with implicit may also connote orthogonal features in other languages. For instance, an implicit function in Scala may define an implicit conversion.

5 Likes

I don't support that narrow definition - for one thing, ternary operators exist and we even have one in Swift! We don't support user-defined ternary operators or allow custom implementations, but in theory there's no reason why we couldn't if there were some compelling use-cases.

but, I agree with the general argument: operators have a slightly different syntax at the point of use, but otherwise they are just ordinary functions. They are just sugar.

  • When calling, I can write, say, 1 + 2 instead of Int.+(1, 2), and
  • When referring to the operator's implementation (as in myInts.reduce(0, +)), I don't need to prefix the member name with a dot (myInts.reduce(0, .+))

And when you read this proposal, the same thing sticks out: myPoly(42) is just sugar around calling a specially-named function member: myPoly.evaluate(42) - or, spelled in more of an operator-y way: Polynomial.evaluate(myPoly, 42) (see my point above about how it's curious that we require operator implementations to be static).

So yes, static callables do not exactly fit within the very narrow range of stdlib-defined operators that we have today, but I think we only need to massage the edges a little bit, and that small expansion would be quite useful generally. The whole idea that there can be operators with an implementation-defined number of parameters with implementation-defined types and where argument labels are preserved is a powerful abstraction that could also help us solve problems like the unspellable requirements in the string-interpolation redesign.

P.S: As for subscripts: there is a reasonable argument that they also should be operators, but that would require an even larger expansion of the operator model.

2 Likes

This proposal was returned for revision by the core team.

Thank you to everyone who participated.

Chris Lattner
Review Manager

2 Likes