Re: Variadic functions and dynamic function calls

On Thu, 10 Dec 2020 at 09:58, Michael Kay <mike@saxonica.com> wrote:

> In my proposal for variadic functions, which has been reasonably well
> received subject to some details, I largely ignored the question of dynamic
> function calls.
>
> Starting point: if dynamic function calls can use keyword arguments that
> map to parameter names, then the keywords need to be included in the
> function signature, and the supplied function must then use the same
> keywords in its declaration as the required function.
>
> This strikes me as something likely to lead to immense complexity and
> considerable usability problems. It's a pain if you can't supply a function
> to an interface if the declaration uses the wrong parameter names. It also
> makes the rules for definining function substitutability (subsumption) even
> more complicated than they already are. I would prefer not to go there.
>

Agreed.


> So what's the alternative? If keywords aren't part of the function
> signature, then I think dynamic function calls can't use parameter names as
> keywords. What are the implications of this?
>
> Without any change to the way function signatures are defined, it's still
> possible to supply a "bounded-variadic" function where the required type is
> non-variadic. For example if the required type is a predicate function --
> `function(item()) as xs:boolean` -- then it's possible to supply the
> bounded variadic function
>
> function increment ($x as xs:integer, $p as xs:integer := 1) {$x+$p}
>
> because a call on increment(12) is perfectly acceptable and meaningful and
> returns 13. In effect, the bounded-variadic function has a non-variadic
> "personality", it is substitutable for the required function type. It is
> also substitutable for the type `function(xs:integer, xs:integer) as
> xs:boolean`.
>

+1

This is in line with my thinking about function references in the other
thread. -- Specifically where a function reference with the same arity as
the number of parameters on the bounded-variadic function creates a
non-variadic reference to the function.


> What about map-variadic and array-variadic functions?
>
> With a map-variadic function, keyword arguments don't map to parameter
> names, they map to entries in the map that is constructed from the call and
> passed as the last argument. So keyword arguments are still possible,
> provided the function signature identifies the function as being
> map-variadic. Rather than adding a "map-variadic" property to the function
> signature, we could treat the function as being map-variadic if and only if
> the declared type of the last argument is a record type e.g. `record(a as
> xs:integer, b as xs:boolean, *)`.
>

I think it makes sense to support both map and record types for
map-variadic functions. The map-variadic functions are also the trickiest
variadic type to define the semantics for.

By my thinking on function references, the function signature it matches
for subsumption rules the last parameter would be the map/record the
function was declared as and follow those rules accordingly -- that is,
mapping parameters of a function signature to keyword arguments is not
supported.

The question then is how the map/record behaves when calling the function.
If we are following that the variadic nature applies at the call site
instead of the declaration site (i.e. building the array/map/sequence
applies at the point at which the function is called), then I don't think
anything else is needed -- the function will be able to be called using
keyword arguments if the last parameter is a map or record.

This would also provide consistency with things like:

   declare function local:test($a as xs:int, $b as map(*)) {};
   let $f := local:test#2 (: or an inline function expression, e.g.
function ($a as xs:int, $b as map(*)) {} :)
   return $f(2, alpha := 1,  beta := 2)

>
> With an array-variadic function, in the current state of the proposal, all
> arguments are supplied positionally, so keywords aren't a problem, except
> for the one case where you want to supply the array argument explicitly as
> an array.
>

My thinking is that the last argument to an array-variadic function is
either an array, or the item-type it is an array over. That is, a function
reference name#arity where arity is the number of parameters in the
array-variadic function is a union-of(array(T), T). For function matching
rules, this would mean that the last parameter can either be an array(T) or
T. This way, both function references and function matching/subsumption
work in the same way (behaving in this instance like sequences).


> I think if we switched to using a sequence rather than an array for this
> kind of function, we could take advantage of the fact that a single item is
> equivalent to a sequence of length 1. So if we defined a function
> `product(xs:double...) as xs:double` as a function that takes a sequence of
> xs:doubles and returns their product, then we could allow both
> product(1,2,3,4) and product((1,2,3,4)) as calls. We could either (a) add
> the "variadicity" of the function to the function signature, allowing
> dynamic calls to take either form, or (b) require the function signature to
> be `function(xs:double*) as xs:double` and allow only the second form in a
> dynamic call, or (c) (more radically) treat function signatures as
> sequence-variadic by default, so if the dynamic function has arity 3, and
> the function call has 5 arguments, then we combine the values of arguments
> 3, 4, and 5 together into a sequence supplied as the value of parameter 3.
>

I think it makes sense to keep array-variadic functions, as there are cases
(such as building JSON objects/arrays) where you would want to preserve the
sequences and not have them flattened.

I tentatively like the concept of sequence-variadic functions from the
perspective of having symmetry across the different types w.r.t. variadic
function support. For consistency with the proposal as defined, a
sequence-variadic function would need to work as option (c), however that
could lead to pitfalls from a user perspective and also lead to potential
bugs if given f(xs:int*, xs:long*) and passing f(1, 2, 3) by mistake and
how would it work if an f(xs:int*) function was also defined (where the
expectation would be it would be a bounded-variadic function `f(xs:int*,
xs:long* := ())`).

If we require sequence-variadic functions to use `...` that would be
better, but then results in an inconsistency in how variadic functions
work. Given the requirement to make this compatible with existing map-based
options I'm happy with using `ItemType...` as a compromise syntax
specifically for sequence-variadic functions (which maps to the `ItemType*`
sequence type in the function definition). -- This way, a function
reference or function signature with the same arity as the number of
parameters in the function will have a last parameter as a sequence type,
supporting passing a sequence of values or a single item, but not
additional parameters.


> So my current thinking is along the following lines:
>
> (A) Make minimal change to function signatures or to the rules for
> subsumption of function types: in particular, don't add parameter names to
> the function type.
>
> (B) A bounded-arity function F can be supplied where the required type has
> a fixed arity N provided that a static call on F with N arguments is
> allowed; a dynamic call with N positional arguments is handled in the same
> way as a static call with N positional arguments; a dynamic call with
> keyword arguments is not allowed.
>
> (C) If the expected type of a function has arity N, then in a dynamic
> function call with M arguments (M >= N-1), the values of supplied arguments
> in positions N, N+1, N+2, etc are sequence-concatenated together and
> supplied as the value for argument N in the function signature.
> Alternative: allow this only if the occurrence indicator of the last
> parameter in the function signature is "...".
>
> (D) Keyword arguments in a dynamic function call are allowed only if the
> last argument in the function signature is a record type (which may or may
> not be an extensible record type); it is possible to supply a value for
> this argument either as a single positional argument evaluating to a map,
> or as a set of keyword arguments matched against the names in the record
> type.
>

+1

These all look reasonable. As noted in my comments above, I prefer (C) to
only apply when the occurrence indicator of the last parameter is
`ItemType...`.

(E) If the expected type of a function has arity N and the last argument is
an array, then in a dynamic function call with M arguments (M >= N), the
values of supplied arguments in positions N, N+1, N+2, etc are combined
into an array of size M-N with the of [aN, aN+1, ..., aM] and supplied as
the value for argument N in the function signature.

(F) If the expected type of a function has arity N and the last argument is
an array, then in a dynamic function call with N arguments where the last
argument is an item type that is a subtype of the item type associated with
the array, a single-item array with argument N as its value is passed as
the value to parameter N.

Kind regards,
Reece


> Michael Kay
> Saxonica
>
>
>
>
>
>
>

Received on Thursday, 10 December 2020 13:01:32 UTC