SE-0253: Static callables

The review of SE-0253: Static callables begins now and runs through April 5, 2019.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager through direct message on the forums.

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thank you for contributing to Swift!

Chris Lattner
Review Manager

18 Likes

good!

  • What is your evaluation of the proposal?

Huge +1. I’ve been looking forward to this feature for a very long time. This proposal is a great start on support for user-defined callable types!

I would like to see the key path types made callable. That seems like an uncontroversial proposal, but it would be easy enough to add in a follow up proposal (unless the core team is willing to amend this one with that small change).

I do hope to see support for static callable added as well to round it out (and for consistency with the static subscripts that look to be coming soon). I also hope implicit conversion to function types will work out eventually.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. The proposal documents several important use cases, some of which I have run into myself. This fills an important gap.

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

Yes, very much so.

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

I don’t have anything to add to what the proposal itself has to say

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

I have participated in all of the discussions related to this topic.

2 Likes
  • What is your evaluation of the proposal?
    +1 Looks good to me.
  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes.
  • Does this proposal fit well with the feel and direction of Swift?
    Yes.
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    This compares favorably to both operator() in C++ and CREATE/DOES> in Forth.
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I've read the pitch, and the pre-pitch thread, and have been considering the idea since @dynamicCallable was proposed.

To try out the proposed feature, here's some toolchains:

Linux Toolchain (Ubuntu 16.04)
Download Toolchain
Git Sha - 8dff4cd
Install command: tar zxf swift-PR-23517-193-ubuntu16.04.tar.gz

macOS Toolchain
Download Toolchain
Git Sha - 8dff4cd
Install command: tar -zxf swift-PR-23517-251-osx.tar.gz --directory ~/

4 Likes

I am favorable to the feature, with the following comments:

  • I feel that there should be a plan to unify call and dynamicallyCall; dynamicallyCall should be a special case of call, like dynamic members are a special case of subscript.
  • I find it weird that callables are explicitly not convertible to function types, and that there is ostensibly no syntax (aside from the closure syntax) to create a callable from a call (whereas, for instance, self.foo creates a closure that binds the self parameter).
  • The Ugly: it will be possible to use ownership qualifiers for variables of callable types, but it still won't be possible for functions.
5 Likes

+1
The only thing I feel missing is ease of discovering that a type is callable.

So would this allow CallableType()() (as in init then immediately call)? I know every language feature can be misused, but this (to me) looks inscrutable.

You can already do that:

func CallableType() -> () -> Void {
    return { print("This really shouldn't block the new feature.") }
}

CallableType()()
7 Likes

I am a big +1 on the idea overall.

I am not in love with the call syntax, so I would like the core team to see if there is something less heavyweight. I also dislike that call sounds like a verb, so it is confusing when used in a context which doesn't actually call anything.

My personal preference for a replacement syntax is:

func _ (...) {}

I think the one big problem with that was you weren't able to reference the function directly. May I propose the following syntax for that:

let refToFunc = myCallable as ()->() //or whatever the call signature is

If you want/need something more magical where you don't have to think through the call signature, then I would suggest:

foo( myCallable as func )  //Foo expects a function

I think both of these syntaxes are clearer than myCallable.call which sounds like it is being called. myCallable as func says what it does on the tin.

2 Likes

+1 on this. Given that we already have dynamic callables, I think it's only fitting that we introduce a static equivalent. IMO it would be fairly peculiar to only have dynamic callables while rejecting static ones.

I'm also a fan of the call syntax for this, in my mind it draws parallels with init/deinit/subscript, being somewhat "meta" actions on an instance. My only wish, and it's one that should not in any way block this proposal, is that it's not possible to directly substitute a callable type with a function type when they have a matching function signature. This isn't a huge desire anyway, since it's still possible to refer to the actual call member.

One question I do have though: Is there any conern for the impact this might have on compile times? My gut tells me this should be a "fairly" simple thing to check since I image it just requires traveling up a type hierarchy, and that computation (I assume) can be cached.

Once again I'd like to shoutout the TensorFlow team for bringing awesome features to upstream Swift! Waiting for when the AD proposal comes up, that will be a big one for programming languages as a whole.

Personally, I think if dynamic callables were to be introduced after this proposal, a natural declaration syntax could be:

calldynamic(arguments: T) -> T {
  ...
}
calldynamic(arguments: [String: U]) -> T {
  ...
}

It is stated in the proposal that

Implicit conversions are generally problematic in Swift, and as such we would like to get some experience with this base proposal before considering adding such capability.

In other words, this proposal is not saying implicit conversion to functions is explicitly banned, but just saying that it is not added as part of this proposal and to be evaluated later, given the complexity of implicit conversions in Swift today.

This is not the case. The proposal has a section about "Direct reference to a call member", to quote it:

Direct reference to a call member

Like methods and initializers, a call member can be directly referenced, either through the base name and the contextual type, or through the full name.

let add1 = Adder(base: 1)
let f: (Int) -> Int = add1.call
f(2) // => 3
[1, 2, 3].map(add1.call) // => [2, 3, 4]
1 Like

In addition to the section that discusses this alternative syntax, I also believe that a func member indicates that it should be accessed via a member reference expression (i.e. dot expression), but the call-syntax is not consistent with that (it's not adder.()).

As to "call sounds like a verb", a lot of named functions sound like a verb as well, and when they are directly referenced they are also not doing anything, for example:

set.formUnion // this is a verb, but it is not calling anything
array.append // this is also a verb, but it is not calling anything

The call member is not different from those cases.

3 Likes

Apologies for the missed part about “.call”. The fact that it’s not implicitly convertible for a stated reason doesn’t make me feel any less weird about it.

I personally believe in the direction of implicit conversion to functions, and I think it is a large design space we'd like to explore as well. We also hope that normal function types can conform to protocols one day like their nominal type function-like counterparts so that they can be used in generic algorithms. For instance, we'd like to be able to treat a function (without model parameters) as a layer.

Good question, let's run some benchmarks. @dan-zheng

Thank you! We expect to send out a differentiable programming pitch some time this year.

I'm happy to clarify the surrounding text to make it not sound explicitly banned.

+1

Yes, I think callables are a very interesting addition to Swift.

Yes. Although I would prefer to have the callable and dynamicallyCallable features and syntax more aligned I like the proposed call keyword.

Read the pitch and the proposal.

  • What is your evaluation of the proposal?

I think it's a great feature, but I don't see why it's implemented using a new declaration instead of func call(…) + an attribute on the type.

New kinds of declarations have a fairly large impact on the language. Their interactions with other features need to be explicitly defined:

  • Can you declare a call...
    • …at global scope?
    • …in a local function?
    • …in an extension?
    • …in a protocol?
    • …in a protocol extension?
  • Can you inherit them from a superclass?
  • Do calls have…
    • …return types?
    • throws?
    • …a failable ??
    • …access control modifiers?
    • mutating modifiers?
    • static modifiers?
    • override? final?
    • …attributes? (Which ones?)
  • Does foo.`call` refer to a call member?
  • How are they represented in mangled names?
  • How are they represented in runtime metadata?
  • Can you fetch one as a bound method? An unbound method? A key path?

The proposal answers (at least most of) these questions; that's not my point. My point is, implementing those answers requires changes to all of our tooling, changes to the ABI and runtime, and changes to tools maintained by volunteers or companies using Swift. And it will need to be considered in future work on features like reflection and macros. In short, it turns this proposal into a much bigger extension of the language than it really needs to be.

Existing decl + attribute has a much smaller impact and implementation effort; it's a better solution if an existing decl's behavior is close to the desired behavior. Here, the only difference we really want between call and func declarations is that we don't want static func call to work. The type checker can easily check that it's not working on a metatype before considering a call function, and the decl checker can just as easily detect and diagnose a static func call in an @callable type, so that's easy to support.

Maybe there's a strong reason to design it this way. For instance, if you imagine that the built-in structural function type could one day be treated as having a call declaration and that this would yield significant type system improvements, that could be sufficiently beneficial to justify the complexity of a separate call declaration. But right now, I just don't see it.

The implementation bears this out. Scroll through the diff and try to get a sense for how much of it actually implements the desired behavior compared to how much teaches various parts of the compiler that call declarations exist. My guesstimate is that only 200-300 of those lines implement and test the type-checking behavior or necessary declaration checking; that leaves 900-1000 lines that only exist to support the new kind of declaration.

I urge you to think about how much shorter and simpler the "Proposed solution" and "Detailed design" sections would be if you were proposing func call + @callable [class/enum/protocol/struct] instead of this new call member and to make your decision accordingly.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes—it would be very useful to make pseudo-functions that are configurable, serializable, or equatable, and I can think of tons of uses beyond that.

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

Yes. This is far more Swift-y than @dynamicCallable.

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

Call syntax is one of the few things that can't be hooked in Ruby and the language suffers for it. Even the built-in types for functions have to overload subscripts or expose .call methods. It's a mess.

(Objective-C is in a similar place, but it doesn't aspire to have a clean, overloadable syntax.)

By contrast, C++ operator() is pretty handy and folks have built a lot of useful things from it.

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

Quick reading.

12 Likes

First let me say I am a fan of this proposal overall, and the tensor flow for swift team's work in particular. I don't think any of this should block acceptance of the proposal, I just want the core team to consider various options around what syntax feels the most natural for Swift...

I have to disagree pretty strongly here, and I think this argument works in favor of my proposed syntax.

let fooVar = myObj.foo //The dot accesses the member foo
fooVar() //The call syntax for a function is ()

As you can see above, the call syntax for a function is the parenthesis (along with any parameters). This is true for top level functions and for methods on classes and structs. The dot syntax allows you to refer to a member of a class/struct/etc...

By making something statically callable, we are saying "treat it like a function". That is, you can call it directly with the normal call syntax for functions

myCallable() //It can be called just like a top level function

I think we agree on that much, the main disagreement is over how to explicitly refer to the function without calling it. I think, because we are treating the callable as a function itself that:

myCallable as func
//or
myCallable as ()->()

...is very natural and does what it says on the tin. referring to myCallable is ambiguous because it can be seen either as an instance of it's type or as a function, so I am telling the compiler which way I want it to be viewed in this instance. Even if we adopt the call syntax, I still think as ()->() should work to refer to the function.

In contrast, if the way to refer to the function is myCallable.call using dot syntax, then we are conflating ideas. I suddenly need to know this new magic word call. What I am treating like a function in one context, I am treating like a member in another context. It breaks the mental model unnecessarily. With as func it is never treated like a member, only as something which can be treated like a function.

I also feel like if someone sees as ()->() or as func in code, they will know what it does from other knowledge of how Swift operates. If they see myObj.call, it is an entirely new concept which has to be looked up. Because we don't have implicit conversion, and it should be fairly common to want to pass these things around as functions (e.g. as completion handlers), I predict that myObj.call will become a top search on Stack Overflow.

As for the argument that func necessitates a dot member lookup, that certainly isn't true of top level functions. I also think it is reasonable to define func _ () as creating a top level function for a type (that can be called directly on the type). I also feel like the difficulty of learning func _ () vs. call for those defining a statically callable type are roughly equal.

3 Likes

One more question. Let's say I have a type with two different calls as follows:

struct A {
    call () {...}
    call (x: Int) {...}
}

and something with the following overloaded function:

struct B {
    func foo (_ fn: ()->() )
    func foo (_ fn: (Int)->() )
}

Then how do I disambiguate which call I want for the following:

   myB.foo( myA.call ) //Which one is called???

with

myB.foo( myA as (Int)->() )

the answer is obvious.

2 Likes