[Pitch #3] Introduce user-defined dynamically "callable" types

I think it's unlikely that a type would want to support both static and dynamic callable. With that in mind, I think separate attributes make the most sense. If somebody really wanted to expose both static and dynamic calls they could just add both @callable and @dynamicCallable to the type.

I'm not able to work on implementation but I would be happy to collaborate with anyone interested this feature. I'll spend some time thinking about this (especially motivation) and start a pitch thread in the next week or two.

I noticed that this proposal does not support both positional and keyword arguments in the same call. Some dynamic languages (such as Ruby) do support this. Is there a reason this proposal doesn't support mixing them or was that an oversight?

1 Like

The implication (or at least my reading) of para 3 under 'Ambiguity resolution' (e: combined with some comments upthread from Lukas + Chris) is that it does support that, that if there are any keywords given then the 'keywords' form is used and positional arguments come in with empty keyword strings.

(This doesn't support compile-time checking of Ruby's "positional arguments must precede keyword arguments" but that is fairly baroque...)

1 Like

The current version Swift seems to trap when initializing a type conforming ExpressibleByDictionaryLiteral with multiple equal keys which would be required to use a dynamicCall with multiple arguments (with an empty string key) and keyword arguments.

Paragraph 3 says:

Similarly, if a type implements both the keywordArguments and the arguments form, the compiler will use the arguments form for call sites that lack keyword arguments, and use the more general form for call sites that do have keyword arguments.

It isn't clear from this that keyword arguments are allowed for some, but not all arguments. I certainly don't see anything that indicates that the positional arguments come with any specific keyword string (such as empty). I think this needs to be spelled out in more detail if it is supported, or clearly stated as a limitation if it is not supported.

The proposal is using DictionaryLiteral which allows duplicate keys. Its is a weird type. Further discussion about it.

func justPrinting<T>( _ input: DictionaryLiteral<String, T>)
{
    print(input)
}

justPrinting(["": 1, "": 2, "": 3]) // DictionaryLiteral<String, Int>(_elements: [("", 1), ("", 2), ("", 3) ])

Oh, interesting :thinking:

Well, the @dynamicCallable feature captures a variable amount of arguments and provides them as one argument to a function, just like the existing ... syntax. So we'd have two very similar features next to each other.

Obviously, variadics at the moment cannot capture keyword argument labels. But when there are no keyword arguments, they can already perfectly package the entire list, right? Or is there something I missed?
That's why I thought adding keyword support to our existing variadics feature might be simpler and also more powerful than introducing a new feature which is just available for structs/classes/etc. but not available for normal functions.
If that is not the case or if the added flexibility is not needed, then so be it.

1 Like

This is a really great point, thank you, I've added this to the proposal!

1 Like

Great catch, fixed!

This was an oversight in the writing. I've clarified it to specify that missing keyword arguments are passed as an empty keyword string. Thank you for pointing this out!

-Chris

1 Like

The method names look very odd to me. I would suggest:

  func dynamicallyCall(withArguments: [T1]) -> T2

  func dynamicallyCall(withKeywordArguments: [String: T3]) -> T4

  func dynamicallyCallMethod(named: S1, withArguments: [T5]) -> T6

  func dynamicallyCallMethod(named: S2, withKeywordArguments: [S3: T7]) -> T8
2 Likes

+1, thanks for the suggestion! Incorporated into the draft proposal.

-Chris

1 Like

@rxwei makes an interesting suggestion, although I do believe Swift naming guidelines elide 'with' for parameter labels.

I'm on the fence about whether the function itself should be a verb ("dynamically call") or simply a noun ("dynamic call"); if we're going to call the attribute @dynamicCallable and not @dynamicallyCallable, I think on balance I'd favor the original.

Swift naming guidelines elide ‘with’ for parameter labels.

@xwu I don't think it exists in the API design guidelines, but I'd appreciate it if you can point me to such writing.

Dynamic calls can be mutating the underlying object. I believe a verb-like name is more appropriate. For example:

let array: PyValue = [1, 2, 3]
array.append(4)

If we desugar this dynamic call, the dynamicallyCall name sounds very reasonable. It's an action (call "append"), and I can't tell whether it's mutating the underlying data or not.

array.dynamicallyCallMethod(named: "append", withArguments: 4)

While a noun-like name below seems to imply that the function is "returning a dynamic call named 'append' with argument 4", without performing the call action.

array.dynamicMethodCall(named: "append", withArguments: 4)

You can see it in the detailed write-up of how the guidelines are applied in SE-0005 and SE-0006. For example:

  • In migrating the standard library from Swift 2 to Swift 3 based on the naming guidelines, joinWithSeparator(_:) becomes joined(separator:).
  • In translating Objective-C names, fillWithBlendMode(_:alpha:) becomes fill(blendMode:alpha:) (and many other examples).

It's impossible to say whether a dynamic call is more likely to be mutating or not, nor do I think it's wise to associate that consideration with the name of the underlying method here. At best, the name could be ambiguous, but ultimately I don't think that the consideration is applicable since a dynamic call self-evidently rules out any way to know for sure.

I think withArguments here conforms with the guidelines. The obj -> swift grand renaming is not part of the guidelines as far as I know. At least I do not see evidence of this in the guidelines.

[Full bikeshed mode enabled]

Shouldn't it be called withParameters since we are defining the the kind of parameters that the function we are calling can take? I guess it is both parameters (the key) and arguments (the value to the key).

I do think that named could be dropped as Method describes the name.

  func dynamicallyCallMethod(S1, withArguments: [T5]) -> T6
// or
  func dynamicallyCall(method: S1, withArguments: [T5]) -> T6

3 Likes

IMO, withArguments conforms to the official guidelines. The guidelines also have an example using with:.

I don't have a strong opinion about named:. On one hand, I agree that dropping would make it simpler. OTOH, given that users will implement this method for syntactic sugar someCallableStuff(), there's no problem with this method being verbose for clarity.

I think "parameter" refers to a formal parameter in the function declaration. "Argument" refers to a value being passed in to match a parameter's type.

From The Swift Programming Language: Redirect

Every function has a function name, which describes the task that the function performs. To use a function, you “call” that function with its name and pass it input values (known as arguments) that match the types of the function’s parameters. A function’s arguments must always be provided in the same order as the function’s parameter list.

It falls under the guideline of “omit useless words”—the Objective-C to Swift renaming was conducted to adopt Swift API naming guidelines and demonstrates how those guidelines are intended to be applied. See SE-0005/6 for more.

Yes, “with:” conforms to the guidelines because, where a label is necessary, it cannot be omitted as then there would be no label. However, where there are other words in the label, “with” is elided, as the parentheses surrounding the arguments imply “with.” See SE-0005/6.

1 Like