Interesting data, but most of typealias Parameters seem to refer to configuration parameters (like this one), which are completely orthogonal to function call parameters...
okay. Let's split the difference ![]()
func dynamicallyCallMethod(_ named: String, parameters arguments: MagicalType) {...}
Thanks for entertaining the idea.
This example seems to suggest that Swift argument labels should specify the roles of types (parameter types), not what's actually passed in (arguments). I don't think that's the case... Argument labels (when they are nouns) specify what's expected to be passed in, in this case, arguments.
Another related name: Things being passed to an executable are called CommandLine.arguments, not CommandLine.parameters.
@Chris_Lattner3, the example in the introduction doesn't match the proposed solution. It should be:
a = someValue.dynamicallyCall(withKeywordArguments: [
"keyword1": 42, "": "foo", "keyword2": 19
])
Nice catch, fixed, thanks!
Have we converged on whether "with" should be included in the name?
@xwu made a good point that "with" falls into the needless words category. I think in the dynamicCall case "with" is still valuable because withArguments: makes it clear that "arguments" are not objects to "call".
I would prefer removing with altogether but even better than withArguments I would suggest:
func dynamicallyCall(with arguments: [T1]) -> T2
func dynamicallyCall(with keywordArguments: [S1: T3]) -> T2
Without "arguments", it's very unclear that the array/dictionary elements are arguments to the dynamic call.
with what else would you make a dynamic call? The current Python interop implementation from Swift for TensorFlow uses this syntax and it is clear enough (PyValue.call(with:)).
In any case this method should not be designed to be manually called but to be clear when reading an implementation.
The current PythonObject.call(with:) is not specifying arguments because:
-
@dynamicCallablesyntax sugar is not implemented and users have to writex.call(with:). If it's too long it's less usable. -
x.call(with:)isn't taking an array or dictionary and is less confusing (because each variadic argument is semantically already an argument).
dynamicallyCall is taking an array/dictionary, not variadics.
dynamicallyCall(with: Array(repeating: 0, count: 10))
Without "arguments", how is it clear that the only argument to the Swift function is an argument list for the dynamic language?
This function is not designed to be used, so clarity is important even if it's verbose.
Using only with to label both positional and keyword arguments would prevent the following:
@dynamicCallable @dynamicMemberLookup
struct JSValue {
func dynamicallyCall(withArguments: JSValue) -> JSValue
func dynamicallyCall(withKeywordArguments: JSValue) -> JSValue
func dynamicallyCallMethod(named: JSValue, withArguments: JSValue) -> JSValue
func dynamicallyCallMethod(named: JSValue, withKeywordArguments: JSValue) -> JSValue
subscript(dynamicMember named: JSValue) -> JSValue
}
extension JSValue: ExpressibleByArrayLiteral {}
extension JSValue: ExpressibleByDictionaryLiteral {}
extension JSValue: ExpressibleByStringLiteral {}
Related API (from Objective-C): JSValue.invokeMethod(_:withArguments:)
As it is not designed to be called, your example from the calling point of view is not very appropriate. We should analyze its clarity and design from the implementation point of view. This...
@dynamicCallable
struct PyObject {
func dynamicallyCall(with arguments: [PyObject]) -> PyObject {
... arguments ...
}
func dynamicallyCall(with keywordArguments: [String: PyObject]) -> PyObject {
... keywordArguments ...
}
}
...is a lot more clear and clean than this:
@dynamicCallable
struct PyObject {
func dynamicallyCall(withArguments arguments: [PyObject]) -> PyObject {
... arguments ...
}
func dynamicallyCall(withKeywordArguments keywordArguments: [String: PyObject]) -> PyObject {
... keywordArguments ...
}
}
Which is extremely verbose, redundant and resembles old Objective-C delegate methods.
Nice find! I am sold. Notice how the drop the label on the first parameter for the method.
func call(withArguments: [Any]!)
Invokes the value as a JavaScript function.
func construct(withArguments: [Any]!)
Invokes the value as a JavaScript constructor.
func invokeMethod(String!, withArguments: [Any]!)
Calls the named JavaScript method on the value.
But those JavaScriptCore APIs are designed to be called directly in your source code.
APIs for @dynamicCallable are designed to be called indirectly, when the compiler translates syntactic sugar. So it doesn't matter if they're verbose, or have redundant labels.
You could probably avoid direct use completely, by calling JavaScript APIs such as apply().
// Swift overlay with @dynamicCallable and @dynamicMemberLookup assumed.
import JavaScriptCore
let context = JSContext()
let function = context.Math.max
let arguments = [5, 6, 42, 3, 7]
// Instead of `function.dynamicallyCall(withArguments: arguments)`
function.apply(nil, arguments)
Note that apply is necessary because Math.max() takes zero or more individual arguments, not an array of arguments.
// JavaScript
Math.max(5, 6, 42, 3, 7) //-> 42
Math.max([5, 6, 42, 3, 7]) //-> NaN
FWIW, I'm not planning on changing the names of the methods in the proposal. This entire bike shed can be repainted in the formal review process.
I most likely didn't read all posts in the "Python-interop" story, but my expectation was that "dynamically callable" would be the equivalent of a function, whereas dynamic member lookup would take care of objects (and expose regular properties as well as methods).
This seems to be possible, but there's also https://gist.github.com/lattner/a6257f425f55fe39fd6ac7a2354d693d#smalltalk-family-languages, which not only causes ambiguity, but also leaves us with three related concepts with two different spellings:
@dynamicMemberLookup, which has directly maps to the subscript requirement, and @dynamicCallable, where you have to choose whether you want to act as a plain function (dynamicallyCall(withArguments:)), or as a message receiver (dynamicallyCallMethod) -- or both.
I think it would be cleaner to split the latter form (@dynamicMessageReceiver??) -- or don't split at all:
I expect that it will be very common to combine @dynamicMemberLookup and @dynamicCallable (that's the plan for PyValue, isn't it?), and a single @dynamic attribute could make the compiler look for a special subscript, just like it's looking for the special methods associated with dynamicCallable (if the subscript is there, you can do dynamic member lookup, and if it's missing, you can't).
Besides consistency, I think a single @dynamic would look much nicer than the combination of the two attributes (which itself seem to be inconsistent: @dynamicCallable describes the target itself, @dynamicMemberLookup describes a feature of it).
This is a very niche feature so I don't think brevity is required. Also, @dynamic could mean a lot of unrelated things, and while writing that I just remembered that dynamic already means something different (dynamic method dispatch for Objective-C interoperation?).
As an update, @dynamicCallable has been implemented! Kudos to @dan-zheng for the implementation. It's made available in the Python interop library on the "tensorflow" branch.
https://github.com/apple/swift/blob/tensorflow/stdlib/public/Python/Python.swift
To try it out, you can download a toolchain from swift/Installation.md at main · tensorflow/swift · GitHub.
PR + formal proposal are coming soon. Next week is likely to be busy for people :-)
Howdy!
The proposal PR is here: [Proposal] Dynamic Callable (`@dynamicCallable` attribute) by dan-zheng · Pull Request #858 · apple/swift-evolution · GitHub
And the implementation PR is here: [Proposal] Dynamic Callable (`@dynamicCallable` attribute) by dan-zheng · Pull Request #858 · apple/swift-evolution · GitHub
Any feedback would be greatly appreciated!
As @rxwei said, @dynamicCallable has already been made available on the "tensorflow" branch and added to the Python interop module. You can test it by downloading a toolchain from swift/Installation.md at main · tensorflow/swift · GitHub.
import Python
let list: PythonObject = [1, 2, 3]
list.append(4)
print(list) // [1, 2, 3, 4]
print(Python.type(list)) // <type 'list'>
let np = Python.import("numpy")
print(np.arange(6).reshape(2, 3))
// [[0 1 2]
// [3 4 5]]