Pitch: Introduce (static) callables

@rxwei that’s not as general as the pitched callable feature - it only supports returning values of Self.

One enhancement you should mention as aa future direction is to make the key path types callable. I think that would be relatively uncontroverisal.

1 Like

Metatype values are not directly callable. The Type(arguments) call syntax is special-cased to behave as if Type.init(arguments). You have to explicitly write the .init if you're initializing from a dynamic metatype.

2 Likes

At first blush, this seems plausible. We allow higher-ranked requirements in protocols and classes already. You may however still run into some of the type inference problems of higher-ranked polymorphism if a function were overloaded on different higher-ranked call constraints.

Extending KeyPath types to have a ‘call’ member is a great idea!

Can I add call members in an extension? The use of a custom declaration and no attribute makes me think "yes", but it's worth calling out explicitly.

Do types with call members implicitly convert to function types?

This feature is a syntactic sugar and is purely additive. It guarantees source compatibility.

Since func call is no longer valid, this is a source-breaking change. How do you plan to stage that in?

10 Likes

+1 on the feature from me.

Some comments about the writing:

  • Another motivation is that we have a (long term) goal of unifying structural types and nominal types, to allow structural types to conform to protocols. The natural way to do this is to the structural type be sugar for a named type (just like [Int] is sugar for Array<Int>), so functions will become nominal types at some point (when varargs and a long list of other issues is sorted out, we aren't particularly close to this). When that happens, it would be nice to handle function calls just by virtue of Function having a call member.

  • Grammar thing: "it is uneasy" -> "it isn't easy"

  • Writing: "Machine learning models often represent a function that contains parameters" -> most readers will think that 'parameters' refers to function parameters here, not "learned constants values closed over by the semantic function" :-).

  • " If model could be called like a function, like what it represents mathematically, the code would look much simpler:" -> the key principle we care about here is clarify of the code. I'd recommend pointing out that clarity is really harmed by the lack of this feature.

  • super nit: in sexpParser(“(+ 1 2)”) you have curly quotes going on.

  • Wording, instead of " But it is also very different from subscript in that:" I'd suggest a positive form of "but it is more similar to a func declaration in that:"

  • " A value cannot be implicitly converted to a function when the destination function type matches the type of the call member. While it is possible to lift this restriction, it is beyond the scope of this proposal." -> I'd suggest being more direct and saying something like "implicit conversions are generally problematic in Swift, and as such we would like to get some experience with this base proposal before even considering adding such capability. The explicit support should be enough to unblock any usecase, so that addition is just a sugar"

  • " It guarantees source compatibility." -> This is taking call as a keyword, you should mention that.

Overall, I'm very excited to see this come together, thank you for pushing this forward!

-Chris

8 Likes

In the case of subscript, there is special behavior currently tied to being a subscript, since subscripts are one of the only lvalue forms allowed in the language. I agree that having a special declaration syntax for call is not as directly motivated; I wouldn't be against a design where you write func call(...) with either an attribute or a declared conformance to a function constraint to give you the syntax sugar.

2 Likes

Awesome! In thinking about this further, wouldn't the syntax you posted basically be syntactic sugar for something like this (which should be possible immediately when the proposal is implemented):

protocol Bound {
     associatedtype T
     call (_ value: T)
}
struct BoundClosure<B: Bound> {
  var function: B
  var value: B.T

  call() { return function(value) }
}

let f = BoundClosure({ print($0), x }) // instantiates BoundClosure<(underlying type of closure), Int>
f() // invokes call on BoundClosure

This sounds acceptable.

I wouldn't want to have to have a method called call. Often times there would be a more appropriate name for the method call form. For example, monoids may want to make combine callable.

In some cases it may be desirable to make a method available while also providing the callable sugar. A method attribute would be most convenient in these cases as it would avoid the need to write a forwarding wrapper. On the other hand, a method attribute forces you to choose an arbitrary method name for all callable signatures.

One way to preserve the convenience in the former case without forcing a concrete method name when you only want the member visible as callable would be to use _ to discard the nominal visibility of the method @callable func _ (x: Int, y: Int) -> Int. If we don't cringe at the use of _ here it would give us control over both whether a nominal method is also provided as well as what the name of that method and would save us from having to write forwarding wrappers when we do want both.

I'm not suggesting this would be a better design. The pitched design feels more clear and explicit. But that clarity does come at the cost of writing forwarding wrappers when we also want a nominal method.

How often do we think we'll want to expose both direct calls and nominal methods? How do we feel about having to write forwarding wrappers in those cases?

4 Likes

I feel like this increases the surface area of learning Swift quite heavily, as it's likely to become a (ab)used feature for aesthetic reasons. Maybe I'm just not used to it, but it seems to make following code difficult with a special-case syntax.

Does it actually need a new keyword call rather than just allowing unnamed functions?

struct Adder {
    var base: Int
    func(_ x: Int) -> Int {
        return base + x
    }
}

or using self / Self:

struct Adder {
    var base: Int
    func self(_ x: Int) -> Int { // instance method
        return base + x
    }
    func Self(_ x: Int) -> Int { // class method
        return base + x
    }
}
5 Likes

Without trying to exacerbate the bikeshedding nature of the conversation, I too like the clear and explicit design pitched here. I also like that call for () parallels subscript for [].

Agree, there's a lot to like about it. The only downside I see is forwarding wrappers and I'm not sure yet how significant that is.

One concern with a special decl is that it becomes less obvious how to reference the call operation as a function value. With a func call, object.call would just work, whereas you'd need to use object. ` call ` or something along those lines if it were a special form.

1 Like

Wouldn't callable types implicitly convert to function types? Function types feel like syntactic sugar for a callable existential with a specific call signature and no other members. Concrete types implicitly convert to existentials so it seems aligned with that behavior to implicitly convert callable types to functions.

If we have this behavior it isn't clear to me when it would be necessary or desirable to reference the call function directly.

2 Likes

That's a possibility, but we'd have to evaluate the impact on type checking performance if we allowed this to be implicit.

2 Likes

Makes sense, I was wondering if it would be a type checking issue. :crossed_fingers:

Can this be unified with @dynamicCallable somehow? What if a type is both static and dynamic callable, which one takes precedence?

You should also keep in mind that adding a new declaration kind has a very high implementation cost, and it would be preferable to work this in via a subscript or method instead.

2 Likes

This implicit conversion might be a bit tricky because function values have reference semantics. So would converting a value type to a function value give you a mutable box?

A function value's context is itself immutable, but can contain references to mutable state. If you have:

var x = 5
let y = 6
return { x + y }

Then the underlying context for { x } is essentially:

class Closure {
  let x: Box<Int>
  let y: Int
}

class Box<T> {
  var value: T
}

so I don't think it'd be inconsistent for a callable value converted to a function to remain immutable.

4 Likes

Can I create protocols with callable requirements?

Also can we omit () if the parameter is a single function and use trailing closure syntax?

extension Optional {
  mutating call(_ update: (inout Wrapped) -> Void) {
    if case .some(var wrapped) = seld {
      self = .some(update(&wrapped))
    }
  }
}

var optional: String? = "Hello"
optional { $0.append(", Swift") }
1 Like

This would be fine with me in general. The only scenario I can imagine finding it surprising is if the callable method was mutating. By making this a new kind of declaration I think it sidesteps this issue (unless we have a reason to allow mutating call).

2 Likes