I think "statically callable" and "dynamically callable" are misleading names because (to me) they suggest that the call is statically/dynamically dispatched, which isn't the case.
I'm not sure about the best names for the two concepts but I'll refer to them as "regular callable" and "dynamically callable" in this post.
"Dynamically callable" is the idea described in the proposal: @dynamicCallable
types are required to define at least one of two dynamicallyCall
methods.
func dynamicallyCall(withArguments: A) -> X
// - `A` can be any type where `A : ExpressibleByArrayLiteral`.
// - `X` can be any type.
func dynamicallyCall(withKeywordArguments: D) -> X
// - `D` can be any type where `D : ExpressibleByDictionaryLiteral` and
// `D.Key : ExpressibleByStringLiteral`.
// - `X` can be any type.
Calls to an instance of a @dynamicCallable
type are desugared to an application of one of the two methods.
@dynamicCallable
is limited for the following reasons:
- By definition, the
dynamicallyCall
methods take a variable number of arguments, so it's not possible to enforce a dynamic call to take a fixed number of arguments within the type system. To implement something like std::plus
and std::minus
, I can only do the following:
@dynamicCallable
enum BinaryOperation<T : Numeric> {
case add, subtract, multiply
func dynamicallyCall(withArguments arguments: [T]) -> T {
// I can't enforce the two argument precondition using the type system.
// What I really want is: `call(withArguments: (T, T))`.
precondition(arguments.count == 2, "Must have 2 arguments")
let x = arguments[0]
let y = arguments[1]
switch self {
case .add:
return x + y
case .subtract:
return x - y
case .multiply:
return x * y
}
}
}
let add: BinaryOperation = .add
add(1, 2) // works
add(1, 2, 3) // fails at run time, not compile time
- The
dynamicallyCall
methods require all arguments to have the same type. This limits @dynamicCallable
to a few specific use cases like dynamic language interoperability.
The "regular callable" concept (I'll refer to it as @callable
) address both of these limitations.
@callable
types can mark any method as callable: there's no constraint on the type of the method whatsoever.
Calls to an instance of a @callable
type are simply forwarded to the callable method.
For the BinaryOperation
example above, @callable
would be more ideal to enforce a two argument precondition:
@callable
enum BinaryOperation<T : Numeric> {
case add, subtract, multiply
@callableMethod // Mark this method as sugar-able.
func call(withArguments arguments: (T, T)) -> T { ... }
}
let add: BinaryOperation = .add
add(1, 2) // works
add(1, 2, 3) // fails to type-check at compile time
@callable
is analogous to operator()
in C++ and is more general than @dynamicCallable
.
However, it doesn't provide a great answer for dynamic language interoperability because argument labels aren't desugared in the same way.
Here's a Python interop demonstration:
// Testing the Python function `str.format(*args, **kwargs)`.
let greeting: PythonObject = "Hello {name}!"
// With @dynamicCallable: desugared to
// `dynamicallyCall(withKeywordArguments: ["name": "John"])`.
//
// Looks natural and similar to Python: `greeting.format(name="John")`.
greeting.format(name: "John")
// With @callable: there is no argument label sugar.
// Need to construct dictionary manually.
greeting.format(["name": "John"])
To me, it seems the main question is: is dynamic language interoperability important enough to Swift to justify @dynamicCallable
?
Note that @dynamicMemberLookup
doesn't have the same problem of generality as @dynamicCallable
, it is just as useful for language interop as other use cases.