-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
XPath: Short-circuiting Functions and Lazy Evaluation Hints #281
Comments
I don't understand this. The arity of I think your intent is probably captured by the definition: there exists at least one value t such that f(t, ?) is a constant. That is to say, it's a function whose result doesn't depend on the value of its argument. I think we can understand intuitively what this means, but I'm not sure how we define it precisely. I can think of other ways of formalising lazy evaluation that might work better. For example we could say that when an argument is declared as Consider the function
and suppose this is called as let $X = 0 return To do this, we would not only need some way of prescribing that the expression passed to $y is not evaluated until its value is needed. We would also need to prevent the optimizer eliminating common sub-expressions by pre-evaluating ($y + 1) outside the conditional. Generally I don't think it's a good idea to try and impose such constraints on the implementation. Apart from anything else, I think we lack the formal semantic machinery to describe such constraints in rigorous language. We're also in grave danger of prohibiting optimisations that existing code unknowingly relies upon. I think the changes we have already made, to define certain constructs as having "guarded" subexpressions, are sufficient to meet the requirement. |
We have a specific example in the text, when XPath 3.1 processors raise an arity-mismatch dynamic error, because an 1-argument function was expected, but a 0-argument function was provided. So even today's XPath 3.1 processors can dynamically determine this case, this is possible. And in this case the XPath 4 processor, implementing the updated function coercion rules, it knows that it has to evaluate the provided 0-argument function and to ignore the argument. This will be done in all cases in XPath 4 because the rules dictate so. The lazy hint is what it is - a "hint" - telling the XPath processor that it will not be necessary in this case to evaluate the argument that is not used. As for: "The arity of If we write our function in the way shown in the proposal, then no hint would be needed because the XPath processor does find the fact that it was passed a 0-arg function when an 1-arg function was expected. But if our function body was just: |
I think it's also worth pointing out that the term "lazy evaluation" is used in two different senses. The first sense, which you seem to be using, is: the expression supplied as an actual function argument is not evaluated unless and until the value of the argument is actually required. The second sense is that when the expression evaluates to a sequence, it is evaluated incrementally, and we only evaluate as much of the sequence as is actually needed. For example, if the only reference is to And what does it mean for the value to be "actually required"? Is the value "actually required" if it is passed as an argument to another function? Or if it is returned as the result of the function? Or if it is used in an "instance of" expression that can be evaluated by knowing the type of the value, but not the actual value? We could, of course, fudge this all by saying that the "lazy" keyword is just a hint and processors can use it or ignore it as they choose. But I think from experience with other hints such as "unordered", we would find that most implementations end up ignoring it, and as a result few users trouble to use it. |
It seems I didn't make myself clear. The arity of the function It might be a function that returns the same result regardless of the value of its argument, but it is still an arity-1 function. |
Actually it isn't. It is a 0-arguments function and both BaseX and Saxon flag this as error in XPath 3.1 Also, please see my additional edits to my reply to your 1st comment. |
Could you give the precise expression that is flagged as an error? |
let $fAnd := function($x as xs:boolean, $y as xs:boolean) as xs:boolean
{
let $partial := function($x as xs:boolean) as function(xs:boolean) as xs:boolean
{
if(not($x)) then ->(){false()}
else ->($t) {$t}
}
return $partial($x)($y)
}
return
$fAnd(false(), true()) |
But there's no partial function application of the form The query fails in Saxon with the error:
Under the proposed coercion rules this query would become legal; under the proposed rules the zero-arity function |
Maybe even better this expression: let $partial_fAnd_1 := function($x as xs:boolean) as function(xs:boolean) as xs:boolean
{
if(not($x)) then ->(){false()}
else ->($t) {$t}
},
$fAnd := function($x as xs:boolean) as function(xs:boolean) as xs:boolean
{
$partial_fAnd_1($x)
}
return
$fAnd(false())( true()) |
I see what you mean. Still the provided expressions that result in arity mismathch in Xpath 3.1, do show us how to write functions with shortcutting. Maybe we need a new language construct that would allow us to specify the exact code implementation of a specific partial application function for a given function. Something like this: let $f := ->($x, $y) {$x and $y}
with partial_arg1 := function($x as xs:boolean) as function(xs:boolean) as xs:boolean
{
if(not($x)) then ->(){false()}
else ->($t) {$t}
} Then the processor will know that there was a specified partial application specified, and if there was a lazy hint will invoke this specified partial application ( |
But the actual arity of false#0 is 0 |
So, we have: declared (or presumed) arity and actual arity I am referring to the actual arity. |
Let us have this expression:
Evaluating If If Because a possibility exists to be able to ignore the evaluation of It is not clear to me whether or not the current evaluation rules require such delay in deciding whether or not to evaluate If the current rules don't require such delayed decision, then this is where a lazy hint comes: it indicates to the XPath processor that it is logical to make the decision about evaluation of |
@dnovatchev Sorry, but this discussion feels very theoretical to me. I can't recollect any user having asked for explicit hints (options, pragmas) to control the laziness/eagerness of XQuery code evaluation. Instead, the expectation of the vast majority is to have the processor choose the best evaluation strategy. Could you please add at least one practical use case to this discussion in which the behavior of the available query processors behaves in an unsatisfactory way without evaluation hints? |
Sure, immediately. This was already shown in the main post. Evaluation time of 100 seconds using one XPath processor vs. 0 seconds using another. To quote again the main post:
|
I’ve seen that example, but counting from 1 to 1000000000000000000 isn’t something that’s done a lot in practice. I was wondering if we can find at least one use case that we can regard as a real-world challenge. |
It can be substituted with any heavy-computation work -- there's plenty of that "in practice", isn't there? Just as an idea, imagine crypto-mining 😄 Or put here any known practical task that is NP-complete. With the lazy hints being followed what was prohibitively expensive (close to impossible) before, becomes now realistic in any shortcutting cases, independent of what XPath processor is being used. |
I’ll still be happy to get convinced. Let’s take the given example for BaseX:
If there are plenty of examples, I think we should be able to make at least one of them replicable. |
This is always going to be the case with a declarative language. Consider join queries, such as Of course, XSLT provides xsl:key to allow users to "hand-optimize" such queries. XML database products provide their own mechanisms to define whether constructs should be indexed or not. But with lazy evaluation, I think implementors are almost certainly better at knowing when to use this technique than 99% of users are. It's worth pointing out that when the poor performance of this query was pointed out to us, we were able to fix the problem very easily without any major design change, and without any need to change the query, let alone the specification. I also believe strongly in something I was taught as a student 50 years ago: the optimization decisions a compiler makes should always be driven by studying real workloads - the programs that typical users actually write. For a product implementor, it's a business decision where to put their R&D investment, and they have a better understanding than standards committees of where that investment is going to get a return. We know, for example, that a small improvement in some operations on XML trees (for example, copying subtrees) is going to benefit a lot more users than a large improvement in operations on arrays. That has to be our decision. |
Alas, we have the real example where from two major XPath processors one fails to perform what can be considered "an almost obvious optimization". That is failure in 50% of a user's possible choice of an XPath processor. If I as an user can choose between two XPath processors one of which is known to perform a certain task in 0 seconds and the other is known to perform the same task in 100 seconds, I will obviously choose to use Processor 1. If, on the other side, I am able to direct Processor 2 how to perform much better in such cases, then I have a richer choice of tools (XPath processors) and even could use them interchangeably. Imagine you are inside a moving fully-autonomous car that has a bug and is colliding with an obstacle, and you could take the wheel and handle the dangerous situation, but you are not allowed to do so because "implementors are almost certainly better at knowing when to use this technique than 99% of users are" and you watch the bad things happening right in front of your eyes... It should be absolutely clear which alternative is the better one. |
@ChristianGruen if about 50 PLs, among them almost all Functional PLs, implement several kinds of short-circuiting operators, are you not convinced that they had sound practical reasons for doing so? Maybe they were all wrong and you are right. I would not comment further on this. |
Proposed chain-able calls looks fine while the each returned function arguments would match its following invocation(arity).
The sequence What happen if the return type would depend of argument?
When arity of returned function does not match arguments count (or arg types ) we could
The syntax of proposed collection as arguments set could vary, one of is to pass as union of arguments.
The trick is to delay evaluation till the moment the arity is known. This concept also applicable for types transformation if in addition to arity the argument types are available.
|
At the call yesterday, I think Dimitre asked two questions, and I'd like to expand on the answers. Given an expression like (a) is there a guarantee that (b) if I think we can simplify the discussion by considering the dynamic function call The rules are given in §4.4.2.1 Dynamic Function Calls:
Is it required that the steps be performed in the order prescribed? No, but it is required that the outcome is the same as if they were performed in the order prescribed. For example if Where is this stated? Well, we don't state it very formally. The closest we come is probably in §2.3.3 "Within each phase, an implementation is free to use any strategy or algorithm whose result conforms to the specifications in this document." which essentially says you can do what you like to evaluate Is function coercion invoked? On the current rules, no. The coercion rules are only invoked in particular specified circumstances (§4.4.3 the situations in which the rules apply are defined elsewhere...) and the rules for dynamic function calls do not invoke them. In general, coercion rules are only invoked where there is an explicit user-declared required type (†), defined for example in a function signature or a variable declaration, and that is not the case here. We could change the rules so that † I think there are cases in XSLT where the coercion rules are invoked where the required type is system-defined, e.g. the value of |
@michaelhkay Thank you for the detailed explanation:
Actually no, the function If the expression is
I propose that we change 1. to:
In case our rules do not include the above phrase ("... and the
As I pointed out above, the variable To summarize:
|
Clearly the composability principle means that the rules for evaluating Certainly the type of |
It can: let $f := function($x as xs:boolean) as function(xs:boolean) as xs:boolean
{
(: Some code here like:
if(not($x)) then ->(){false()}
else ->($t) {$t}
:)
} Here from the declaration of $f we know that it returns a function with arity 1 that takes a boolean and returns a boolean. The XPath programmer also knows that the returned function could be with 0-arity, thus the programmer can indicate this using a lazy hint: return
$f($x) lazy ($y) |
Under the current specification, the arity-zero function (Note, I believe that the effect of the rules in §2.4.4 Errors and Optimization is that the function Not evaluating an argument that isn't referenced in the function body is a pretty straightforward optimization, and I'm not sure why you think the hint is useful. It's potentially more useful if the argument is referenced in some but not all execution paths, but I'm still very doubtful that many programmers would use it wisely. In your example the "lazy" hint is present unconditionally: it also applies if the function to be evaluated turns out to be It might be useful if you wrote a more detailed proposal, in spec prose, defining the syntax and semantics of the new construct: and perhaps a note suggesting advice to users on when to use it. |
Actually §2.4.4 appears to contain a bit of a contradiction on this. First it says:
Subsequently it says:
The second extract implies that, given the function |
That is OK as the processor has established that the argument will not be used, so the processor will not evaluate it (especially if there is a lazy hint) But I do see some contradiction in this rule: If the processor has evaluated E only "in part", then in the general case it might not be able from the result of this partial evaluation "to establish that the actual value of the operand E does not violate any constraints on its cardinality"
I agree, and I also believe that we need to fix this rule not to require/expect the processor to do any checks in this case at all. |
Absolutely true! This is why it is the programmer who decides whether or not to specify the hint, depending on the nature of the problem and the algorithm that is implemented -- it would not be possible for the XPath processor to automatically deduce whether or not a delay in the evaluation is justifiable. For example, we could assume that the probability of the 1st argument of Again, the programmer (me 😄) should be in control. Thank you for the very useful and needed explanation of the current rules! |
@michaelhkay Thank you for your guidance and encouragement. As I have never written such prose till now, would you, please, recommend a specific existing example of such that I could follow? |
I am closing this as the comments thread has become too-long. I have produced the "prose" that @michaelhkay asked for and it is the core of the new issue #299 Thanks to all who provided feedback - this is how the whole idea was refined to its current, more precise form in #299 |
Short-circuiting Functions and Lazy Evaluation Hints
1. Introduction
As shown in Wikipedia, most contemporary programming languages offer reasonable support for short-circuit evaluation
(also known as minimal or McCarthy evaluation), including several standard language short-circuit operators.
Short-circuiting, as we will call the above in this document, is commonly used to achieve:
excessive evaluation time or throwing an error
Usual example, using a C-based language:
Consider the following example:
In this example, short-circuit evaluation guarantees that
myfunc(b)
is never called. This is becausea != 0
evaluates tofalse
. This feature permits two useful programming constructs.If the first sub-expression checks whether an expensive computation is needed and the check evaluates to false, one can eliminate expensive computation in the second argument.
It permits a construct where the first expression guarantees a condition without which the second expression may cause a run-time error.
Idiomatic conditional construct
Perl idioms:
2. Short-circuiting in XPath
In short (pun intended) there is no such thing mentioned in any officially-published W3C version (<= 3.1) of XPath.
This topic was briefly mentioned in the discussion of another proposal: that of providing the capability to specify strictly the order of evaluation.
Aspects of incorporating hints for lazy evaluation (a topic related to short-cutting) were discussed also in the thread to this question on the Xml.com Slack.
The situation at present is that the XPath processor that is being used decides whether or not to perform shortcutting, even in obvious cases. Thus, varying from one XPath processor to another, the differences in performance evaluation could be dramatic. For example, the following XPath expression is evaluated on BaseX (ver. >= 10.3) for 0 seconds, and the same expression is evaluated by Saxon ver. 11 for about 100 seconds.
3. Analysis
We can define the term “function with shortcutting” (just for a 2-argument function, but this can be extended for
N
-argument function whereN >= 2
) in the following way:Given a function
$f($x, $y)
, we denote in XPath its partial application for a given value of $x (saylet $x := $t
) as:$f($t, ?)
The above is a function of one argument. By definition:
$f($x, $y)
is equivalent to$f($x, ?) ($y)
, for every pair$x
and$y
.That is, the partial application of the 2-argument function
$f
with fixed 1st argument is another function$g
which when applied on the 2nd argument ($y
) of$f($x, $y)
produces the same value as$f($x, $y)
:If
$g
is defined as$f($x, ?)
, then$g($y)
produces the same value as$f($x, $y)
for every pair$x
and$y
.Let us take a specific function:
Then one equivalent way of defining
$fAnd
is:The
$partial
function is the result of the partial application$fAnd($x, ?)
and by definition this is a function of arity 1, which when applied on the 2nd argument of$fAnd
, produces the same result as$fAnd($x, $y)
From the code above we see that actually there exists a value of
$x
(the valuefalse()
) for which$fAnd($x, ?)
is not a function of one argument, but a constant function (of 0 arguments) – that produces the valuefalse()
.Definition:
We say that a function
f(x, y)
allows shortcutting if there exists at least one valuet
such thatf(t, ?)
is a constant.4. Solution
How can an XPath processor treat a function with shortcutting?
Obviously, if the XPath processor knows that
f(x, y)
allows shortcutting, then it becomes possible to delay the evaluation of the 2nd argumenty
and only perform this evaluation if the arity of the function returned byf(t, ?)
is1
, and not0
.How can an XPath processor know that a given function allows shortcutting?
One way to obtain this knowledge is to evaluate
f(t, ?)
and get the arity of the resulting function. XPath 3.1 allows getting the arity of any function item with the function fn:function-arity(). However, doing this on every function call could be expensive and deteriorate performance.Another way of informing the XPath processor that a given function
f(x, y)
allows shortcutting is if the language provides hints for lazy evaluation:let $fAnd := function($x as xs:boolean, lazy $y as xs:boolean) as xs:boolean
Only in the case when there is a lazy hint specified the XPath processor will check the arity of
f(x, ?)
and will not need to evaluate they
argument if this arity is0
.Let us return to the original example:
Executing this with an Xpath 3.1 processor, an error is raised: “1 argument supplied, 0 expected: function() as xs:boolean { false() }.”
But according to the updated “Coercion Rules / Function Coercion” in Xpath 4.0, no error will occur:
This is exactly the place where the XPath processor will call the lower-arity function without providing to it the ignored, and not needed to be evaluated, additional argument.
Thus, according to this rule, an XPath 4.0 processor will successfully evaluate the above expression and will not issue the error shown above.
Finally, we can put the lazy hint on a function declaration or on a function call, or on both places:
How to write short-circuiting functions?
The code above is a good example how one can write a short-circuiting function evaluating which the XPath processor would be aware that a short-circuit is happening but instead of signaling arity error as an XPath 3.1 processor does, will logically ignore the unneeded 2nd argument.
The text was updated successfully, but these errors were encountered: