Pitch: Introduce (static) callables

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

The pitch has a section on types that both have call members and are marked with @dynamicCallable:

Basically, call members take precedence over @dynamicCallable methods.


I don't think exactly unifying call members and @dynamicCallable behavior is possible because @dynamicCallable provides special sugar for argument labels:

// With `@dynamicCallable.
np.random.randint(-10, 10, dtype: np.float)
// With `call` methods. The empty strings are killer.
np.random.randint(["": -10, "": 10, "dtype": np.float])
1 Like

I like this pitch, except that I would prefer a callable type to be declared as:

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

Because:

  • A callable's implementation is still a function, and _ is already being used to mark the absence of a name, especially in function argument labels.
  • This does not add a new keyword, which some have complained about elsewhere.
4 Likes

Just because I hate underscores ;-):
It would also possible to use func (_ x: Int) -> Int instead, wouldn't it?

2 Likes

In response to @Dante-Broggi and @Tino above:

Using a nameless func or func _(...) is possible, but to me they don't convey "callable method" as clearly as an explicit call keyword.

Also, we want to support direct references to callable methods via foo.call. This makes more sense when callable methods are declared with the call keyword, and is consistent with init declarations/direct references (e.g. foo.init).

3 Likes

I do like the clever use of self, and I think it can be applied to subscripts too, showing the operator.

func self[_ x: Int] -> Int { // subscript
    return base + x
}

And you can directly reference them via x.self(_:) and x.self[_:].

In any case, I believe the call declaration syntax and the subscript declaration syntax need to be consistent. The current subscript declaration syntax is already being used everywhere—changing this has significantly higher cost.

2 Likes

Thanks. All comments addressed.

Yes. I added some clarification about this.


Nope. I incorporated some rationale suggested by Chris:


Changed the "source compatibility" section. I think a warning+fixit would do it.

I like the feature. The main ugly parts are breaking source compatibility and stealing the short and probably useful function name “call”. Is there a reason why you couldn't allow both call and func call with only the former being “callable”? You could disallow name clashes in the usual way, and possibly warn if the two are mixed on the same type. I suppose subscript doesn't work like that but I'm not sure why. Is it ambiguous or just confusing?

It's not 100% ambiguous but anything like this makes it harder to recover when parsing erroneous or incomplete code. As a further example, this would no longer be valid:

func call(_ fn: () -> Void) {}
call() {}

because that call() {} now looks like a declaration rather than a use! (Albeit a misplaced one, since it's not on a type.) You really want to ban such a thing.


I'm still not convinced that the new declaration kind is the way to go. Another potential reason I thought of today (besides the source compat problem) is chaining:

struct Addition {
  func __call__(_ a: Int, _ b: Int) -> Int {
    return a + b
  }
}

struct Identified<Operation> {
  var __call__: Operation
  var identifier: String

  init(_ op: Operation, identifier: String) {
    self.__call__ = op
    self.identifier = identifier
  }
}

let add = Identified(Addition(), identifier: "+")
print(add(1, 2))

But maybe this is getting too clever. You can always get the same effect with a closure or explicit wrapping call function.

(I'm not actually proposing __call__.)


I'm also a little concerned about type checker performance because we have to assume that basically anything is callable now and prove that it's not, but that's probably okay in practice because type expressions already were.

3 Likes

I've seen some positive feedback on the call syntax, so @dan-zheng and I will continue to implement the feature as proposed! Some of the concerns around the proposed syntax can be discussed in a proposal review once our implementation is done.

4 Likes

That makes sense, thanks.

A quick skim of Github shows a fair amount of Swift code using “call” as a method name. Some of these uses would probably benefit from being ported to be callable, e.g.

public func call(_ name: (Selection, Any) -> (), _ argument0: Any) -> Selection {
  name(self, argument0)
  return self
}

func call(selector:String)

public func call(parameters: [SQValueConvertible]) -> SQValue {
  return call(this: vm.rootTable, parameters: parameters)
}

and some clearly wouldn't and would have to rename this everywhere

protocol Phone {
  func call() -> String
}

protocol Animal
{
  var legs:Int {get set}
  func call(callstr:String) -> String;
}

A lot of the latter use seemed to be in beginner code (which is underrepresented on Github), because this is a short and natural method name. This is unfortunate, because it's not a great experience when learning to immediately hit a keyword collision and have to rename your method or call it with backticks everywhere. This is in contrast to subscript, which seems like a much less likely collision (unless you're in the typesetting domain). Is there a longer/different keyword that would work here instead?

3 Likes

Thanks for the search!

Some alternatives I have in mind are:

  • invoke(): It is much less obvious to me that this implies the call syntax.
  • apply(): There could be even more collisions with this one. Also Swift doesn't officially use "function application" to describe function calls.
  • parentheses()

If there's consensus that source breakage or the ambiguity call brings is not acceptable, then I'll explore the path of changing both subscript(...) and call(...) to self[...] and self(...) respectively.

3 Likes

One obvious alternative, given that dynamic callable uses dynamicallyCall methods, is staticallyCall. This collides with the other meaning of static for functions though, so maybe someone can think of a better adverb.

Ditto. I think that "static" is too confusing and therefore didn't consider it as an alternative. I also believe any adverb would make this first-class syntax feature sound less first-class.

Two other alternatives:

  • selfcall(x: T) -> T
  • selfCall(x: T) -> T
1 Like

I suppose I don't find the motivating examples very compelling.

Wouldn't this example be better served by having an apply operator rather than static syntax (naming would go a long way too)?

input <> conv <> maxPool <> flatten <> dense

It seems like proper naming plus an operator would provide the superior readability. Just so I understand, this doesn't add any additional capability to the language, it just provides "sugar" for those cases where we want to call a method but not pay the price of naming or calling the method?

1 Like