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

Right.

Well I guess that's me; I wouldn't call my bindings library production ready yet but should get there soon.

As long as this proposal sets up the direction I don't mind adding the trailing closure part as a follow-on, if you prefer to concentrate on the fundamental parts for now (of which Ruby would make good use.)

1 Like

So the caveat here would probably be that, at this moment, the Ruby dynamicCall would have to take Any instead of, say, RubyObject as argument type, since function types can't conform to protocols, right?

We'd also probably have to specify the parameter and return types, even if the closure could conform to a protocol.

The more practical approach would probably be to give your RubyObject, or some type conforming to some protocol, an initializer taking a closure and use trailing closure syntax to call that, so your call would look something like this:

myRubyValue.foo(a, b, c, RubyObject { f in stuff(f) })
1 Like

Ah yes, that is a good point. I believe there is a long term desire to allow non-nominal types like functions and tuples to conform to protocols. I'm not sure how that will work out in practice though.

-Chris

2 Likes

I'm really sorry, but I still don't understand what you mean. What "another way" to represent variadic arguments are you referring to?

I can sort of see what you mean in that variadics in Swift implicitly turns the ... into an array of elements, and this is sort of like this. OTOH, what this proposal is really doing is packaging-up/boxing an entire argument list into one value, which isn't something you can do with variadics and definitely not with keyword argument labels.

I tend to see this as a very different thing and for a very different purpose than the problem that variadics solve.

-Chris

I've seen APIs which simulate argument labels, by taking an object literal.

// JavaScript

function example(args) {
  var first = args.first || 0 // default is zero.
  // etc.
}

example({first: 1, second: 2, third: 3})

So a JavaScript bridge might want to support both styles (arguments and keywordArguments).

The revised proposal contains:

We write the arguments and keywordArguments parameter as an dictionary type, but these will actually be allowed to be any type that conforms to the ExpressibleByDictionaryLiteral protocol (which is inclusive of Dictinary, DictionaryLiteral, and other custom types), where the element type has the specified constraints.

But should the arguments still take an array literal?

We write the arguments parameter as an array type, but these will actually be allowed to be any type that conforms to the ExpressibleByArrayLiteral protocol, where the element type has the specified constraints.

We write the keywordArguments parameter as an dictionary type, but these will actually be allowed to be any type that conforms to the ExpressibleByDictionaryLiteral protocol (which is inclusive of Dictionary, DictionaryLiteral, and other custom types), where the element type has the specified constraints.

Agreed, that's the workaround I mentioned somewhere above. It would be more natural for both Swift + Ruby programmers though not to have the extra text.

Explicit modelling of the trailing closure in the dynamicCall implementation also provides further compile-time checking + user convenience in the spirit of the pitch: in Ruby the trailing closure has different syntactic meaning (Ruby 'block') to a closure passed as a method argument.

So although with the workaround we could write:

myRubyValue.foo(a, b, c, RbBlock { f in stuff(f) })

...with a/b/c/RbBlock conforming to RbObjectConvertible, the dynamicCall implementation has to check at run-time whether the last argument is actually an RbBlock to figure whether it is a method argument or a block (trailing closure). And validate the non-last arguments to make sure they were not misplaced RbBlocks.

All doable of course!

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