The errors occurs with the current logic because the JSDynamicMemberLookup protocol does not define a subscript(dynamicMember:) method, it only lists it as a requirement.
The following does compile, however:
@dynamicMemberLookup
public protocol JSDynamicMemberLookup {}
extension JSDynamicMemberLookup {
subscript(dynamicMember name: String) -> JSValue {
get { fatalError() }
set { fatalError() }
}
}
This behavior is easier to implement: @dynamicMemberLookup (and @dynamicCallable) is simply a sugar for a concrete method. Supporting the protocol requirement case is non-trivial.
I'm curious what your use case for a protocol like JSDynamicMemberLookup is?
Iâm worried about the fact that the proposal has relegated support for Smalltalk-like languages to the future directions section. I think itâs important for this proposal be able to support writing bindings to Ruby and Iâm concerned that if itâs not done now, it may never be done.
I see, good point!
Indeed, with the current design there is no way to (usefully) extend an existing type with @dynamicMemberLookup or @dynamicCallable behavior.
This is a notable deficiency in cases where one can't change the declaration of the existing type.
Here, I might argue that the proper solution is for JavaScriptCore to implement @dynamicMemberLookup and @dynamicCallable behavior.
Your concern is valid. It seems to me the only major language interop fully supported by @dynamicCallable at the moment is Python. However, many people are interested in Ruby/Objective-C/JavaScript interop, which all require @dynamicMemberCallable behavior.
Personally, I don't see @dynamicMemberCallable as something that's in danger of never being done. I have a clear idea of how to implement it and I'm happy to share ideas if anyone's interested in tackling it. Existing code for dynamic member lookup/callable behavior should definitely be reused/generalized.
I might tackle it myself over a weekend, when I get a chance.
+1 to the proposal. It's an important feature with many intriguing applications, and seems like a good design.
The use of ExpressibleByDictionaryLiteral is particularly elegant, allowing implementations which can assume unique keywords to use dictionaries, while not ruling out those that can't.
I'll echo Davidâs concerns about @dynamicMemberCallable being left behind. It does seem best to review it separately, but that proposal should not lag too far behind the heels of this one.
This confused me:
This proposal does not introduce the ability to provide dynamically callable static/class members.
Does this refer to something like MyType.dynamicThing("foo")? If so, wouldnât that be supported via static var dynamicThing: MyCallableType? If not, what does it mean?
dynamicCall(withKeywordArguments:) ignores this direction in the language's evolution. The semantics of these calls are fundamentally quite different from a normal Swift function. I worry that, as we try to evolve the AST to improve the extremely messy way we currently handle Swift calls, we may hit limits imposed by this proposal. I don't want to end up in a situation where we're handling Swift code worse in order to handle Python code better.
My other concern about this design is that it is laser-focused on Python. The proposal gestures at how one feature might be used in Javascript, but it's not ideal for that. It doesn't handle Smalltalk and Ruby at all (and IMHO, misses the main challenge of Rubyâit doesn't need to access argument labels early, it needs to handle ambiguity between calls and property accesses). It doesn't help us with any problems outside language bridging.
On a bikeshedding front, I worry that the distinction between "dynamic callable" and "dynamic function" (as in the dynamic keyword) may be too subtle, and that if the @dynamicMemberCallable feature suggested in "future directions" is later adopted, the distinction between @dynamicCallable, @dynamicMemberCallable, and @dynamicMemberLookup will also be too subtle.
I think this functionality should be implemented as two orthogonal features:
Add a variadic list of argument labels to the lookup done by @dynamicMemberLookup. This would let you look up a member whose name includes argument labels.
Add a way to use plain function call syntax on a non-function type, along the lines of C++'s operator (). Python would declare one of these with a variadic list of arguments, but the feature would be able to serve many other use cases, too.
This design would be worse for Python, but better for everything else. It would slot more readily into Swift's semantics and it would cover many more use cases. The downsides are that it makes the Python bridge's job harder (it would need to save the labels seen in the @dynamicMemberLookup call inside the instance it returned), and that a user would not be able to assign a PyObject representing a function to a variable and then call it with keyword arguments using the ordinary function syntax (which they wouldn't be able to do with a Swift closure, either). I think a little bridge awkwardness is worth having a more flexible, semantically simpler feature.
I think good dynamic bridging is a great thing to add to the language, but I don't think this particular design is right for Swift. It is too limiting and too limited. I urge the core team to reject SE-0216 and ask for a different solution to the same problem.
As I understand them based on this proposal and past discussion, @dynamicMemberCallable plus @dynamicMemberLookup will handle Ruby just fine. A Ruby bridge would not use @dynamicCallable at all.
@dynamicMemberCallable is not part of this proposal, so if we need @dynamicMemberCallable to handle Ruby, this proposal doesn't handle Ruby. That's my pointâthe proposal is very narrowly targeted at handling the needs of a Python bridge, and is too inflexible to handle even minor variations in requirements.
Could you expand on possible "limits imposed by this proposal", or problematic AST changes that you envision?
I think, from an implementation perspective, @dynamicMemberLookup and @dynamicCallable are exceptionally simple because they are syntactic sugar. This would make them flexible to changes in the AST.
Can you share a use-case where it's necessary in a language to look up a "member whose name includes argument labels"?
I explored "regular callable" behavior and I agree that it is more general.
However, for keyword-argument calls, it doesn't desugar argument labels like @dynamicCallable does. This is unnatural for language interop:
// With @dynamicCallable.
np.random.randint(-10, 10, dtype: np.float)
// With regular callable sugar. The empty strings are killer.
np.random.randint(["": -10, "": 10, "dtype": np.float])
I think "regular callable sugar" and @dynamicCallable solve different problems and should be evaluated separately.
Perhaps the solution is as simple as not creating a new attribute @dynamicMemberCallable, but rather extending @dynamicCallable to handle the two new dynamicallyCallMethod methods using ambiguity resolution. Would this address your concern?
To be fair, the proposal does say as much. I see this as a family of three proposed features which when taken together do appear to handle all the dynamic language bridging needs, at least all the ones I'm aware of â and do so within an overall division of labor that makes sense to me.
Now if the third (@dynamicMemberCallable) proposal never materializes, then I do share your concern! Assuming it does, however, I donât see a problem with this overall design vis-a-vis Ruby.
Thatâs not to comment on your other concerns; I just donât see that lack of Ruby support in particular should count against this proposal. Thatâs more of a comment on Ruby itself, which has does not have any notion of callable things separate from method dispatch.
@beccadax's argument seems to me to be that due to SE-0111 and SE-0155, Swift itself is becoming a language where it's necessary to look up a "member whose name includes argument labels".
Notionally, a call in Swift has two separate steps:
Look up the callee.
Call it with some arguments.
In theory, CallExpr should just have a callee expression, a list of argument expressions, and a few more odds and ends. In practice, it currently has a lot more than that, and most of it is redundant:
getArg(), the argument tuple
getArguments(), the individual argument expressions
getArgumentLabels(), the labels for the arguments
getArgumentLabelLocs(), the locations of the labels
The argument tuple is kind of transitional and I believe we hope to move away from it in the future, but the labels and label locations are also redundant. When a callee supports argument labels, dealing with them is primarily the responsibility of the callee. That means those labels and locations should already be present in a DeclName and DeclNameLoc somewhere beneath getFn(), so there's no need to have them in CallExpr.
@dynamicCallable is an exception. Because @dynamicCallable allows argument labels to be applied to arbitrary expressions, we cannot be certain that the callee contains an appropriate DeclName and DeclNameLoc for the argument label information, and we'd need to keep it in the CallExpr.
(This is all based on my understanding, which could be wrong.)
If you mean a language whose bridge would require the argument labels to look up a member, the obvious answer is Smalltalk derivatives, whose selectors would most likely be bridged by mapping names like insert(atIndex:) to insertAtIndex:. Smalltalk derivatives are a notable use case on Apple platforms because Objective-C has a Smalltalk-derived design, and Mac apps have often used Smalltalk-derived scripting languages to match it.
It's also worth noting that if you wanted to write a lightweight "SwiftScript" designed to pair well with Swift, you would need this design. If your bridging features wouldn't really support a language very like your own, that seems like a red flag to me.
I agree that this is awkward. The way I would handle it would be something like:
let randint = np.random.randint
randint(-10, 10, dtype: np.float) // callables can't have argument labels
But you couldn't do that if randint were a Swift method, either. You'd have to write:
let randint = np.random.randint(_:_:dtype:)
randint(-10, 10, np.float)
Which would work on both a pure Swift type and a Python-bridged type.
Edit to add:
That would make the proposal cover more language designs, but then I would point out that it lacks coherenceâit proposes four separate mechanisms to cover the four use cases we've thought of with nothing to really unify them. It's a brute-force solution to a problem that seems like it should have a more elegant one.
Thanks for the detailed explanation and your forward-facing thinking!
I completely agree with your assessment of argument labels and that the current CallExpr representation stores them redundantly.
If CallExpr is changed so that argument labels are stored in the callee (rather than the argument), I feel that the current implementation of @dynamicCallable can adapt without significant changes.
Argument-label information will still be available in the AST somewhere: all @dynamicCallable does is extract that information and create a new CallExpr.
More importantly, I agree about the importance of representing "methods whose name includes argument labels". As you said, this seems critical for supporting Smalltalk-derived languages and isn't supported in the current @dynamicCallable implementation at all.
Your idea of introducing a new member lookup method subscript[dynamicMember:argumentsLabels:] and implementing "regular callable behavior" seems to solve this problem.
I like the spirit behind this solution, but I think it lacks polish. Some issues are:
I think the desugared "dynamic call" mechanism is better represented as a function, rather than a subscript method. The proposal mentions using subscript methods:
It was suggested that we use subscripts to represent the call implementations instead of a function call, aligning with @dynamicMemberLookup. We think that functions are a better fit here: the reason @dynamicMemberLookup uses subscripts is to allow the members to be l-values, but call results are not l-values.
With a subscript method, there must be an extra check that no setter is defined, only a getter. This issue doesn't exist with func dynamicallyCall by definition. It'd look like:
Adding a layer of indirection between argument labels and arguments solves the problem of representing "methods whose name includes labels", but it adds complexity.
The dynamic call method is not as easily understandable as before. For many people, it won't be obvious why the method returns a closure or why it's necessary to pass argument labels and arguments separately (especially for Python, where it's not relevant for interop).
The equal number of arguments and argument labels is no longer enforced by the type system, so a runtime precondition check is necessary.
Direct calls to the argument-label-taking dynamic call method are now killer.
Direct calls are necessary when you want to pass arguments as an Array or Dictionary(Literal) instead of variadic arguments. You then must write:
let print = Python.print
// Pretend I read some command-line arguments.
let arguments: [String : PythonObject] = ...
// Current @dynamicCallable implementation.
print.dynamicallyCall(withKeywordArguments: arguments)
// Oh dear. This is Python?
print.dynamicMethod(argumentLabels: arguments.keys)(arguments.values)
We can partially mitigate this problem by defining two versions of dynamicMethod corresponding to withArguments: and withKeywordArguments: so calls without keyword arguments don't need to specify Array(repeating: nil, count: x) for argument labels.
However, the additional complexity here is undeniable.
Do you have ideas for simplifying things? I think it's great to continue iterating!
As Review Manager, I'd like to remind the community that this thread is for review feedback on SE-0216, which is about dynamic freestanding calls. The design of a dynamic method call feature is not on-topic here except to the extent that it influences SE-0216. For example, if you believe that dynamic freestanding calls should be treated as a special case of dynamic member calls (taking the base method name as an optional value, maybe), that would be reasonable feedback to leave here. Otherwise, the development of a design for dynamic method calls should be carried out in its own pitch thread.
Regarding the dynamicallyCall(withKeywordArguments: D) method, should the requirement for D.Key change to be an optional ExpressibleByStringLiteral type?
Effectively, this means that new function signatures would look like:
func dynamicallyCall(
withKeywordArguments: DictionaryLiteral<String?, X>
) -> X
// One positional argument, one keyword argument.
x.dynamicallyCall(withKeywordArguments: [nil: arg1, "label": arg2])
// Problem: what does the empty string label mean?
x.dynamicallyCall(withKeywordArguments: ["": arg3])
The downside to using Optional for argument labels is that it creates a degenerate case, which is the empty string label. (If nil is the proper way to represent "no label", then "" should not be allowed as a label. However, there's no way to enforce this within the type system anymore.)
Thus, I think the idea of using a non-optional type for labels is more robust.
Here's another idea, which isn't core to the proposal.
Better fix-its should be provided when @dynamicCallable required methods are not satisfied.
I believe this is only relevant to Xcode and other tools that take advantage of "diagnostics editor mode".
In Xcode, great fix-its are produced when protocol requirements are missing from a conforming type.
Users are given the ability to add protocol stubs:
Clicking the "Fix" button creates a stub with editor placeholders:
@dynamicCallable (and @dynamicMemberLookup) should also offer such fix-its: it's especially important because the attribute requirements aren't straightforward. (All that's necessary is to implement a function like printProtocolStubFixitString).
One question is: what should the generated stubs look like?
The @dynamicCallable method requirements are unlike typical protocol requirements.
For example, in the func dynamicallyCall(withArguments: A) -> X method, A is required to conform to ExpressibleByArrayLiteral but is not required to be any specific type.
Any of the below are valid:
func dynamicallyCall(withArguments: [Any]) -> Any
func dynamicallyCall(withArguments: Set<AnyHashable>) -> Any
func dynamicallyCall<A : ExpressibleByArrayLiteral>(withArguments: A) -> Any
So what template should the generated stub follow?
Edit: to be clear, the "holes" that need to be filled are the argument type, the return type, and possibly a generic signature.