June 30, 2018·5 min read·engineeringswiftinteroplanguage-design

Swift accepts dynamic calls with structural checks

SE-0216 accepts callable values for dynamic-language bindings. The design retains compile-time checks on argument structure while deferring the foreign call's meaning to a library.

Swift's acceptance of dynamically callable types includes a deliberate restriction on how much dynamism reaches a call expression. A binding can accept positional arguments while rejecting keyword arguments at compile time. The foreign language may determine what a call means at runtime, but Swift can still check whether the binding permits that form of call.

John McCall's June 30 acceptance announcement makes that distinction central to the decision. SE-0216 is accepted without modifications. The Core Team considers the ability to reject structurally invalid calls an important design goal, including rejecting argument labels for a binding that does not support them.

The proposal by Chris Lattner and Dan Zheng places the mechanism in an interoperability design. Most language-specific behavior belongs in Swift libraries. The compiler supplies syntax that lets a value represented by such a library appear in an ordinary call expression. Acceptance establishes the language direction, rather than announcing a stable release containing the feature.

I like Swift's use of types to make constraints visible. Dynamic-language interoperability needs a different set of constraints from a normal Swift function, but it still benefits from stating the constraints that remain available. The distinction between an unsupported argument form and a foreign runtime error is useful to preserve.

A value can represent a foreign callable

A dynamic-language binding commonly represents foreign values through a Swift wrapper. The proposal illustrates the problem with Python values projected into a PythonObject type. A member lookup can produce another wrapped value, but calling that value previously needs a separate method spelling in the binding.

The proposal's motivating examples use explicit calls such as Dog.call(with: "Brianna"). Repeated call operations make the Swift expression longer than the foreign operation it represents. This is especially apparent when calls and member lookups alternate in one expression.

SE-0216 introduces @dynamicCallable to allow the wrapper value itself to use call syntax. The compiler rewrites that syntax into a call to one of the wrapper's dynamicallyCall methods. The binding remains responsible for interpreting its arguments and invoking the foreign runtime.

This division of responsibility limits what the language feature claims to implement. It does not provide a complete Python, Ruby or JavaScript runtime bridge. The syntax hook allows a library to offer a more direct expression of a call. Conversion rules and foreign exceptions remain decisions for the binding.

The proposal explicitly leaves some binding design choices outside its scope. Its examples are sketches of how interoperability could use the facility. That is a useful constraint on reading the code examples as an application author. A plausible expression is not itself evidence that a complete binding implements every operation shown.

The argument container describes the accepted form

The proposed callable methods distinguish positional arguments from keyword arguments. The positional form accepts a type expressible by an array literal. Its keyword form accepts a type expressible by a dictionary literal, with keys expressible by string literals. These protocol requirements give the compiler a way to package call arguments for the library.

Argument element types remain part of the Swift method declaration. The feature does not erase every type involved in the call. A binding can choose an argument representation appropriate to its foreign values, and the compiler must still construct the argument container required by the method.

A type offering only the positional method cannot be called with keyword arguments. That failure is detectable before the foreign runtime receives anything. If the keyword form is available, positional arguments can use empty-string keys in the keyword representation. The two forms therefore express a real difference in permitted call structure.

When both methods exist, the proposal prefers the positional method for a call containing no keyword arguments. A call containing keywords uses the keyword form. If only the keyword method exists, it can handle an entirely positional call through the empty-key convention.

This is a more specific design than accepting an arbitrary list of untyped tokens. The compiler recognizes the call's structure and chooses a method with declared Swift requirements. The library then receives a representation it has agreed to handle.

Dynamic member calls remain separate work

The acceptance discussion identifies the largest omission. Dynamically typed member calls were part of the original pitch but became a future direction. Several reviewers worried that separating them would substantially delay useful interoperability, especially with Ruby and JavaScript.

McCall says the Core Team agrees that a delay is likely. The reason for the separation is implementation difficulty in the current type checker. Freestanding calls were straightforward to implement, while dynamic member calls were difficult. The team accepts incremental progress instead of holding the current proposal until the related work is ready.

This is not merely a naming distinction. A member lookup followed by a call of the resulting value is one possible model. Some dynamic-language method calls need the member name and the call information considered together. The proposal discusses this distinction through Smalltalk-derived languages such as Ruby.

The Core Team also addresses the concern that the eventual member-call design could force a redesign of the accepted mechanism. Its position is that the member-call design is already substantially developed and the delay is primarily an implementation problem. The team considers unifying the mechanisms unlikely to be desirable if doing so weakens structural rejection of invalid calls.

That rationale defines the accepted scope more clearly than treating every future direction as part of SE-0216. A binding author gains a mechanism for calling wrapped values. The additional method-dispatch mechanism remains separate work.

General callable Swift objects are another design problem

Reviewers also asked why Swift would add dynamically typed call behavior before general statically typed callable objects. The acceptance announcement treats those as different language designs with different use cases. Interest in one does not establish that the other can be added through the same proposal.

A dynamic bridge needs a variable-sized collection of arguments represented through its wrapper types. A general Swift callable object may want a fixed signature with unrelated parameter types. Those requirements place different demands on the type system. The accepted proposal concentrates on the interoperability case.

My preference is for this scope to remain explicit in library documentation. An attribute that makes call syntax available can look more general than the mechanism it actually supplies. Naming the argument-container contract helps explain both the convenience and the restrictions without implying the compiler understands the foreign function's full signature.

The accepted design therefore preserves a division between Swift's structural checks and a library's runtime interpretation. A positional-only bridge can reject labels during compilation. A bridge that accepts keyword arguments can receive their names through its declared container. Determining whether the foreign target recognizes those arguments remains the responsibility of the binding and foreign runtime.