possible use cases for fn:apply()

On the joint call of 25 November I took an action to try to work out a
simple use case for the fn:apply() function requested by the OP, as
sketched by Michael Kay in [1] and as commented on by John Snelson in
[2].

[1] https://lists.w3.org/Archives/Member/w3c-xsl-query/2014Nov/0019.html
[2] https://lists.w3.org/Archives/Member/w3c-xsl-query/2014Nov/0026.html

These notes begin with a general remark on the risk of
over-engineering the solution and continue with three examples of the
possible use of fn:apply().  I am sending them to the public-qt-comments
list instead of posting them into the Bugzilla record, because they are
rather long for a Bugzilla comment.


1 Does fn:apply() resolve the issue?

Both MK and JPCS have suggested that fn:apply() is not really useful
unless we also have variable-arity functions and/or partial function
application that doesn't require that we know the function arity
statically and/or variable-argument inline functions.

So it may be worth noting that as far as I can tell the OP is not
asking for variable-arity functions or the like and MK's proposal does
what the OP asked for. He asks for a function that takes a function as
its first argument and some representation of a collection of function
arguments as its second argument, and which would apply the function
to those arguments, along the following lines (adapted from comment
0):

  $f = function my-add($a,$b,$c) { $a + $b + $c }
  $args = [1,2,3]
  fn:apply($f, $args) == $f(1,2,3)

It may well be that the WGs do not have the resources to add all the
functionality mentioned by MK and JPCS. If we restrict ourselves to
resolving the issue actually raised and providing the functionality
actually requested, the burden seems somewhat lighter.


2 Currying

JPCS would like to use fn:apply#2 to curry functions, but does not see
a way to do it without additional functionality. 

I may be wrong, but it seems to me that the following should work:

  (: local:curry($f) accepts an argument of arbitrary arity 
     and returns a function g of arity 1 such that
     - g($a) = $f($a), if function-arity($f) = 1
     - g($a) is a currying of $f, if function-arity($f) > 1
  :)
      
  declare function local:curry(
    $f as function(*)
  ) as function(*) {
    local:curry-helper($f,[])
  };

  (: local:curry-helper takes a function and an array of arguments.
     If the array is the right size for the function, we apply
     the function.  Otherwise, we return a unary function which
     adds one more argument to the array and tries again.
  :)
  declare %private function local:curry-helper(
    $f as function(*),
    $args as array(*)
  ) {
    if (array:size($args) eq function-arity($f))
    then fn:apply($f,$args)
    else function($nextArg) {
      local:curry-helper($f,array:append($args,$nextArg))
    }
  };

(Warning: this may reflect assumptions about our variable scoping that
don't match our spec.)

Note that a similar approach could, I think, be used to implement the
fn:partial-apply($function,$argnumber,$arg) function mentioned by
JPCS.


3 Function composition

I don't see a way to handle John's function-composition story, except
to say that the composition of f and g is not a function h whose arity
matches that of f, but a function h of arity 1, expecting an array
whose size matches the arity of f.

  declare function local:compose(
    $f1 as function(*), 
    $f2 as function(*)
  ) as function(*) {
    let $arity := fn:function-arity($f2)
    return
  
    function($argarray as array(*)) {
      if (array:size($argarray) ne function-arity($f1)) then
         fn:error(xs:QName("func:FNDY0002"),
           "Wrong size of argument array")
      else
         $f1(fn:apply($f2, $argarray))
    }
  };

This is, I guess, a little less convenient than it would be in a
language with variable-argument functions. But it does seem to be
fully generic in a way that is not currently possible at all.


4 Meta-circular interpretation

One well-known use for a higher-order 'apply' function is in
partnership with 'eval' in a meta-circular interpreter (or any kind of
interpreter, I guess). The core of my action item was to work out an
example in enough detail to tell whether fn:apply() is by itself
enough for such an application, or whether it requires additional
functionality like variable-arity functions.

    Note: It may be useful to say explicitly that by a meta-circular
    interpreter, I mean an interpreter written in language L for some
    identical or similar language L' (quite often L' is a core subset
    of L), which (a) can interpret itself and (b) uses the existing
    facilities of the host interpreter for L to support whatever parts
    of L' have the same semantics in L and L'. When L = L', this
    appears to be mostly a parlor trick; when the semantics are
    slightly different, this is a useful way to experiment with
    modifications to the language.

I have not written a meta-circular interpreter for XSLT or XQuery(X),
though it would be a rewarding exercise to do so. Instead, I have done
something often presented as a first step towards a meta-circular
interpreter: an evaluation function for simple arithmetic expressions
expressed in XQueryX.  This suffices, I think, to illustrate the
utility of fn:apply() as described in [1] for this application.

The arithmetic expressions supported are (in intent, at least) roughly
those matching AdditiveExpr in the XQuery grammar, with two
restrictions:

  - Path expressions are restricted to primary expressions (no
    step expressions, no filters, no lookups, no ArrowPostFix
    expressions).

  - Primary expressions are restricted to numeric literals and
    function calls.

Supporting other forms of path expressions and primary expressions
would make the code longer without making it materially more complex,
except that for the evaluation of path expressions and variable
references, the evaluation function will need an argument for the
environment consisting of the static and dynamic contexts.  Adding
such an environment parameter does not dramatically change the
character of the code.

Without fn:apply(), the arithmetic evaluator looks something like this
(N.B. since I don't have convenient access at the moment to an XQuery
3.1 implementation, this code has not actually been run in this form;
a simpler version that doesn't use arrays did run):

  module namespace ev = "http://blackmesatech.com/2014/meta-circularity/arith-eval";
  
  declare namespace xqx = "http://www.w3.org/2005/XQueryX";
  
  declare function ev:eval (
    $e as element()
  ) as xs:anyAtomicType {
    (: handle numbers ... :)
    if ($e/self::xqx:decimalConstantExpr) then
        xs:decimal($e/xqx:value/string())
    else if ($e/self::xqx:integerConstantExpr) then
        xs:integer($e/xqx:value/string())

    (: handle unary + and - :)
    else if ($e/self::xqx:unaryPlusOp) then
        ev:eval($e/xqx:operand/*)
    else if ($e/self::xqx:unaryMinusOp) then
        -1 * ev:eval($e/xqx:operand/*)

    (: handle infix operators :)
    else if ($e/self::xqx:addOp 
             or $e/self::xqx:subtractOp
             or $e/self::xqx:subtractOp
             or $e/self::xqx:multiplyOp
             or $e/self::xqx:divOp
             or $e/self::xqx:idivOp
             or $e/self::xqx:modOp
           ) then 
        ev:apply(
          fn:resolve-QName($e/name(), $e),
          array { (ev:eval($e/xqx:firstOperand/*),
                   ev:eval($e/xqx:secondOperand/*)) } 
        )

    (: handle function calls :)
    else if ($e/self::xqx:functionCallExpr) then
        ev:apply(
          fn:resolve-QName(concat(
                            ($e/xqx:functionName/@xqx:prefix, 'fn')[1], 
                            ':', 
                            $e/xqx:functionName/string())),
          $e
        )

    (: otherwise, give up and blame the user :)
    else error(QName('http://example.com/pierre','ERR99999'),
               concat('You call that an arithmetic expression?! ',
                  'Ha!  ',
                  'I SPIT upon your so-called arithmetic expression!')) 
  };
  
  declare function ev:apply(
    $QN as xs:QName,
    $operands as array(*)
  ) as xs:anyAtomicType {
    let $ns := namespace-uri-from-QName($QN),
        $ln := local-name-from-QName($QN),
        $op1 := $operands(1),
        $op2 := $operands(2),
        $f := function-lookup($QN, array:size($operands))
    return
    if (not($ns eq "http://www.w3.org/2005/XQueryX")) then
      error('bad namespace')
    else if ($ns eq "http://www.w3.org/2005/XQueryX") then 
      if ($ln eq 'addOp') then
        $op1 + $op2
      else if ($ln eq 'subtractOp') then
        $op1 - $op2
      else if ($ln eq 'multiplyOp') then
        $op1 * $op2
      else if ($ln eq 'divOp') then
        $op1 div $op2
      else if ($ln eq 'idivOp') then
        $op1 idiv $op2
      else if ($ln eq 'modOp') then
        $op1 mod $op2
      else error(concat('unknown operator ', $ln))
      (: end $ns eq .../XQueryX :)
    else if exists($f) then
      if ($ns eq "http://www.w3.org/2005/xpath-functions") then
        if ($ln eq 'abs') then
           abs($op1)
        else if ($ln eq 'ceiling') then
           ceiling($op1)
        else if ($ln eq 'floor') then
           floor($op1)
        (: etc, etc, etc :)
        else if ($ln eq 'format-number' and array:size($operands) eq 2) then
           round($op1, $op2)
        else if ($ln eq 'format-number' and array:size($operands) eq 1) then
           round($op1)
      (: end fn:*(...) :)
      else error (concat('unknown function {', $ns, '}', $ln))
    else error('unknown function or operator')
  };

With fn:apply(), as described, the ev:apply() function becomes a
little simpler.  Instead of writing:

    else if exists($f) then
      if ($ns eq "http://www.w3.org/2005/xpath-functions") then
        if ($ln eq 'abs') then
           abs($op1)
        else if ($ln eq 'ceiling') then
           ceiling($op1)
        else if ($ln eq 'floor') then
           floor($op1)
        (: etc, etc, etc :)
        else if ($ln eq 'format-number' and array:size($operands) eq 2) then
           round($op1, $op2)
        else if ($ln eq 'format-number' and array:size($operands) eq 1) then
           round($op1)
      (: end fn:*(...) :)
      else error (concat('unknown function {', $ns, '}', $ln))
   
it can simply say:

    else if exists($f) then
      fn:apply($f, $operands)
    else error (concat('unknown function {', $ns, '}', $ln))
     
I count that as a relatively large improvement.


5 Conclusion

I agree with Michael Kay and John Snelson that variable-arity
functions would make fn:apply more useful.  But I seem to find
fn:apply() useful enough to include in 3.1, even without
variable-arity functions.

-- 
****************************************************************
* C. M. Sperberg-McQueen, Black Mesa Technologies LLC
* http://www.blackmesatech.com 
* http://cmsmcq.com/mib                 
* http://balisage.net
****************************************************************

Received on Saturday, 29 November 2014 02:22:10 UTC