- From: C. M. Sperberg-McQueen <cmsmcq@blackmesatech.com>
- Date: Fri, 28 Nov 2014 19:21:43 -0700
- To: public-qt-comments@w3.org
- Cc: "C. M. Sperberg-McQueen" <cmsmcq@blackmesatech.com>
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