SE-0253: Static callables

Puts compiler implementer hat on
One of the reasons I like having an attribute on the type definition itself (and not a member) is that the type checker doesn't having to go digging through all of the members of the type to determine whether it might be something it can call. It can check the attributes and, if not @callable, quickly conclude that a value of that type is (or is not) callable. It sounds like a small thing, but the type checker makes this "can potentially call this thing?" determination a lot when it's type checking an expression, and particularly in large Swift modules, having to parse the bodies of type definitions in other source files just to establish whether there is a @callable method there can add up to a compile-time performance problem.

Doug

4 Likes

This is the kind of issue where we as a community have to trust the core team to make the right decision. If this makes a “line in the sand” difference (like with union types) then so be it. But otherwise I don’t think it leads to the best programmer model.

Thanks for your perspective!

This is interesting. I think the "spirit" of call declarations is to make a first-class declaration kind, just like how init and subscript are first-class declaration kinds - this is core.

I definitely agree that the ability to quickly conclude whether a type is callable is valuable. But on the other hand, declarations don't define "semantics" in the same way that protocol requirements denote semantic requirements.

It might also be valuable to know, at a glance, whether a type is subscriptable, but "the fact that a type defines a certain declaration kind" isn't modeled as an explicit type annotation, it's an implicit quality of the type because it defines a subscript. Same for first-class call declarations.

1 Like

As Jordan notes, having a special declaration kind or a name (init, func _, func call, etc.) ameliorate my concern somewhat.

FWIW, I have the same concerns that Brent expresses about introducing a new declaration kind. I've said it elsewhere (e.g., in the custom attributes pitch), but introducing a new declaration kind has a very high cost, both in cognitive load and in the implementation. I strongly prefer that we make these normal functions with a type-level attribute to tag it.

Doug

1 Like

I see, that makes sense. (Finding call-syntax delegate methods is more efficient.)

I would like to reiterate some naming difficulties with using "normal named methods" to represent call-syntax delegate methods. If you haven't read it already, please read this post if you have some time. I would be curious about your thoughts.

TLDR: it is difficult to name "normal named methods" in a way such that both 1) applications using direct method references and 2) call-syntax applications are idiomatic.

struct Model {
    @callDelegate func applied(to input: Float) -> Float { ... }
}
model.applied(to: x) // this is fine.
model(to: x) // but leads to this, which is not fine.

// Dropping the argument label makes call-syntax applications idiomatic:
// `func apply(_ input: Float) -> Float` enables `model(x)`.
// But this violates naming guidelines (imperative verbs imply mutability).

Unnamed call-syntax delegate methods make way more sense, something like:

  • Underscore name: @callDelegate func _(...).
    • Requiring @callDelegate methods to have no name (_) may be good. I think that is my ideal alternative.
  • Extra parsing rules to support no name at all: @callDelegate func(...).
    • I dislike the idea of adding new special case grammar for func declarations.
2 Likes

This whole proposal is about objects that can act as something else, isn‘t it?
That is a concept which is quite general, and I wonder wether we may want a general solution:
There has been a proposal for objects to act as an Optional (Introducing `Unwrappable`, a biased unwrapping protocol), and I guess many people would be happy about a way to do calculations with implicit conversions (Float acting as Double...).

Afaik, there are no plans to add such forwarding to Swift, but I‘m also not aware of a decision that those capabilities are unsolicited, so imho it‘s an alternative worth to be considered.

If there is only one way to name the call function (be it func _ or func call or whatever), it's not really an issue: developers that want a prettier direct method reference syntax can provide a separate API with that fully-elaborated name.

Doug

3 Likes

Yes, providing separate APIs with a fully-elaborated name is fine. Not exposing poorly-named call-syntax delegate methods is my main concern, which seems solved by using unnamed call-syntax delegate methods (call(...), @callDelegate func _(...), etc).

Pretty sure we're on the same page, being extra pedantic to leave nothing tacit. :slight_smile:


Minor update: I anticipate @rxwei or I will have time to update the proposal today, so that direct references via foo.call are no longer supported, and to fill in details about ABI impact. Thanks again for everyone's feedback! I think discussion has been very fruitful.

2 Likes

Will putting the attribute on the type ever allow conditionally callable types? Think something like:

@callable
extension Future where  Value: Callable {
    call func<Arg>(_ arg: Future<Arg>) -> Future<Value.CallableResult<Arg>> {
        return self.then { fn in
            arg.then(fn)
        }
}

The interoperation of this proposal and generics is what's caused most doubt in me. People have asked whether it'll be possible to use callability as a constraint (like I also did above with Value: Callable), but I think conditional conformance is equally important.

Even if it won't be possible to use callables in the generic context in the first design, I think it'd be a shame if the chosen syntax made it impossible to extend the design in the future.

Conditionally callable types are (naturally) possible with first-class call declarations: just define a conditional conformance containing a call declaration. You can do the same with subscripts today.

Conceptually, the definition of "call-syntax delegate methods" are what make values callable - a type attribute is a non-essential marker (that comes with type-checking efficiency benefits, and arguably clarity benefits, as mentioned by @Douglas_Gregor).


We argue against a Callable protocol representing types whose values are callable:

Rather than creating a Callable protocol, a more principled and general direction is to implement function types using protocols with required call-syntax delegate methods. See Rust and Scala for precedent in other languages.

AFAICT, this didn't seem to work in the attached implementation. I tried the macOS toolchain, but maybe I didn't try hard enough.

Defining call members in constrained extensions worked for me. I used the macOS toolchain.

extension Array where Element == (Int) -> Int {
  call(_ input: Int) -> Int {
    return reduce(input) { result, fn in fn(result) }
  }
}

let chainedFunctions: [(Int) -> Int] = [
  { $0 * $0 }, // square,
  { $0 + 1 },  // then add one.
]
for i in 1..<10 {
  print(chainedFunctions(i))
  // 2, 5, 10, 17, 26, 37, 50, 65, 82
}

Toolchains:

2 Likes

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.

Terms of Service

Privacy Policy

Cookie Policy