Skip to content
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

1341 Drop position from fold callbacks #1867

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Drop position from fold callbacks
michaelhkay committed Mar 12, 2025
commit 446b93281dcab555df20abd3ef397277c8d0fbf5
2 changes: 1 addition & 1 deletion specifications/xpath-datamodel-40/src/xpath-datamodel.xml
Original file line number Diff line number Diff line change
@@ -522,7 +522,7 @@ therefore can also be contained within sequences.</p>

<example role="signature">
<proto class="dm" name="iterate-sequence" return-type="item()*">
<arg name="input" type="array(*)"/>
<arg name="input" type="item()*"/>
<arg name="action" type="function(item(), xs:integer) as item()*"/>
</proto>
</example>
257 changes: 106 additions & 151 deletions specifications/xpath-functions-40/src/function-catalog.xml
Original file line number Diff line number Diff line change
@@ -14948,7 +14948,7 @@ filter($input, fn($item, $pos) { $pos gt 1 })
<p>The function returns the value of <code>(1 to $count) ! $input</code>.</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left(1 to $count, (), fn($result, $next) { $result, $input })
for-each(1 to $count, fn($item, $pos) { $input })
</fos:equivalent>
<fos:notes>
<p>If <code>$input</code> is the empty sequence, the empty sequence is returned.</p>
@@ -20903,10 +20903,8 @@ return map:of-pairs($ann)
of the result of these calls, preserving order.
</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, (), fn($result, $next, $pos) {
$result, $action($next, $pos)
})
<fos:equivalent style="dm-primitive">
dm:iterate-sequence($input, $action)
</fos:equivalent>
<fos:examples>
<fos:example>
@@ -20970,11 +20968,8 @@ fold-left($input, (), fn($result, $next, $pos) {

</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, (), fn($result, $next, $pos) {
if ($predicate($next, $pos))
then ($result, $next)
else $result
})
for-each($input,
fn($item, $pos) { if ($predicate($item, $pos)) {$item} })
</fos:equivalent>
<fos:errors>
<p>As a consequence of the function signature and the function calling rules, a type error
@@ -21037,7 +21032,7 @@ return filter(
<fos:proto name="fold-left" return-type="item()*">
<fos:arg name="input" type="item()*" usage="navigation"/>
<fos:arg name="zero" type="item()*"/>
<fos:arg name="action" type="fn(item()*, item(), xs:integer) as item()*" usage="inspection"/>
<fos:arg name="action" type="fn(item()*, item()) as item()*" usage="inspection"/>
</fos:proto>
</fos:signatures>
<fos:properties>
@@ -21053,40 +21048,24 @@ return filter(
<fos:rules>
<p>If <code>$input</code> is empty, the function returns <code>$zero</code>.</p>
<p>If <code>$input</code> contains a single item <code>$item1</code>, the function calls
<code>$action($zero, $item1, 1)</code>.</p>
<code>$action($zero, $item1)</code>.</p>
<p>If <code>$input</code> contains a second item <code>$item2</code>, the function then calls
<code>$action($zero1, $item2, 2)</code>, where <code>$zero1</code> is the result after
<code>$action($zero1, $item2)</code>, where <code>$zero1</code> is the result after
processing the first item.</p>
<p>This continues in the same way until the end of the <code>$input</code> sequence; the final result is
the result of the last call on <code>$action</code>.</p>

</fos:rules>
<fos:equivalent style="xquery-function">
declare %private function fold-left-2(
declare function fold-left(
$input as item()*,
$zero as item()*,
$action as function(item()*, item()) as item()*
) as item()* {
if (empty($input))
if (empty($input))
then $zero
else fold-left-2(tail($input), $action($zero, head($input)), $action)
};

declare function fold-left(
$input as item()*,
$zero as item()*,
$action as function(item()*, item(), xs:integer) as item()*
) as item()* {
let $numbered-input := for-each($input, fn($item, $pos) {
{ 'item': $item, 'position': $pos }
})
return fold-left-2($numbered-input, fn($zero, $pair) {
$action($zero, $pair?item, $pair?position)
})
else fold-left(tail($input), $action($zero, head($input)), $action)
};

(: Note: a practical implementation can optimize for the case where the
supplied $action function has arity 2 :)
</fos:equivalent>
<fos:errors>
<p>As a consequence of the function signature and the function calling rules, a type error
@@ -21099,13 +21078,18 @@ declare function fold-left(
<p>This operation is often referred to in the functional programming literature as
“folding” or “reducing” a sequence. It typically takes a function that operates on a pair of
values, and applies it repeatedly, with an accumulated result as the first argument, and
the next item in the sequence as the second argument. Optionally the <code>$action</code>
function may take a third argument, which is set to the 1-based position of the current
item in the input sequence. The accumulated result is
the next item in the sequence as the second argument. The accumulated result is
initially set to the value of the <code>$zero</code> argument, which is conventionally a
value (such as zero in the case of addition, one in the case of multiplication, or a
zero-length string in the case of string concatenation) that causes the function to
return the value of the other argument unchanged.</p>
<p>Unlike other functions that apply a user-supplied callback function to successive
items in a sequence, this function does not supply the current position to the callback function
as an optional argument. If positional information is required, this can be achieved by
first forming the sequence <code>$input ! {'position': position(), 'item': .}</code>
and then applying the <function>fn:fold-left</function> function to this sequence.
(This idiom is frequently used within this specification in defining the formal equivalents
of other functions such as <function>fn:filter</function>.)</p>
Copy link
Contributor

@ChristianGruen ChristianGruen Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore, I assume, thanks to using fn:for-each.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, noted.


</fos:notes>
<fos:examples>
@@ -21196,7 +21180,7 @@ declare function fold-left(
<fos:result>{ 1: 2, 2: 4, 3: 6, 4: 8, 5: 10 }</fos:result>
</fos:test>
</fos:example>
<fos:example>
<!--<fos:example>
<fos:test>
<fos:expression><eg>
let $input := (11 to 21, 21 to 31)
@@ -21209,23 +21193,15 @@ return fold-left($input, (),
</eg></fos:expression>
<fos:result>11, 12</fos:result>
</fos:test>
</fos:example>
</fos:example>-->
</fos:examples>
<fos:changes>
<fos:change issue="516" PR="828" date="2023-11-14">
<p>The <code>$predicate</code> callback function accepts an optional position argument.</p>
</fos:change>
<fos:change issue="1171" PR="1182" date="2024-05-07">
<p>The <code>$predicate</code> callback function may return an empty sequence (meaning <code>false</code>).</p>
</fos:change>
</fos:changes>
</fos:function>
<fos:function name="fold-right" prefix="fn">
<fos:signatures>
<fos:proto name="fold-right" return-type="item()*">
<fos:arg name="input" type="item()*" usage="navigation"/>
<fos:arg name="zero" type="item()*"/>
<fos:arg name="action" type="fn(item(), item()*, xs:integer) as item()*" usage="inspection"/>
<fos:arg name="action" type="fn(item(), item()*) as item()*" usage="inspection"/>
</fos:proto>
</fos:signatures>
<fos:properties>
@@ -21241,42 +21217,26 @@ return fold-left($input, (),
</fos:summary>
<fos:rules>
<p>If <code>$input</code> is empty, the function returns <code>$zero</code>.</p>
<p>Let <code>$itemN</code> be the last item in <code>$input</code>, and let <code>$N</code>
be its 1-based ordinal position in <code>$input</code> (that is, the size of <code>$input</code>).
The function starts by calling <code>$action($itemN, $zero, $N)</code>.</p>
<p>If there is a previous item, <code>$itemN-1</code>, at position <code>$N - 1</code>,
the function then calls <code>$action($itemN-1, $zeroN, $N - 1)</code>, where <code>$zeroN</code> is the result
of the previous call.</p>
<p>Let <var>I/n</var> be the last item in <code>$input</code>, and let <var>Z/n</var> be <code>$zero</code>.
The function starts by calling <code>$action(<var>I/n</var>, $zero)</code>, producing
a result <var>Z/n-1</var>.</p>
<p>If there is a previous item, <var>I/n-1</var>,
the function then calls <code>$action(<var>I/n-1</var>, <var>Z/n-1</var>)</code>, producing
the result <var>Z/n-2</var>.</p>
<p>This continues in the same way until the start of the <code>$input</code> sequence is reached; the final result is
the result of the last call on <code>$action</code>.</p>
the value <var>Z/0</var>.</p>

</fos:rules>
<fos:equivalent style="xquery-function">
declare %private function fold-right-2(
declare %private function fold-right(
$input as item()*,
$zero as item()*,
$action as function(item(), item()*) as item()*
) as item()* {
if (empty($input))
then $zero
else $action(head($input), fold-right-2(tail($input), $zero, $action)
};

declare function fold-right (
$input as item()*,
$zero as item()*,
$action as function(item()*, item(), xs:integer) as item()*
) as item()* {
let $numbered-input := for-each($input, fn($item, $pos) {
{ 'item': $item, 'position': $pos }
})
return fold-right-2($numbered-input, fn($zero, $pair) {
$action($pair?item, $zero, $pair?position)
})
else $action(head($input), fold-right(tail($input), $zero, $action))
};

(: Note: a practical implementation can optimize for the case where the
supplied $action function has arity 2 :)
</fos:equivalent>
<fos:errors>
<p>As a consequence of the function signature and the function calling rules, a type error
@@ -21298,9 +21258,13 @@ declare function fold-right (
<p>In cases where the function performs an associative operation on its two arguments (such
as addition or multiplication), <function>fn:fold-right</function> produces the same result as
<function>fn:fold-left</function>.</p>
<p>The value of the third argument of <code>$action</code> corresponds to the position
of the item in the input sequence. Thus, in contrast to <function>fn:fold-left</function>,
it is initally set to the number of items in the input sequence.</p>


<p>Unlike other functions that apply a user-supplied callback function to successive
items in a sequence, this function does not supply the current position to the callback function
as an optional argument. If positional information is required, this can be achieved by
first forming the sequence <code>$input ! {'position': position(), 'item': .}</code>
and then applying the <function>fn:fold-right</function> function to this sequence.</p>
</fos:notes>
<fos:examples>
<fos:example>
@@ -21334,7 +21298,7 @@ declare function fold-right (
<fos:result>"$f(1, $f(2, $f(3, $f(4, $f(5, $zero)))))"</fos:result>
</fos:test>
</fos:example>
<fos:example>
<!--<fos:example>
<fos:test>
<fos:expression><eg>
let $input := (11 to 21, 21 to 31)
@@ -21349,7 +21313,7 @@ return fold-right(
</eg></fos:expression>
<fos:result>12, 11</fos:result>
</fos:test>
</fos:example>
</fos:example>-->
</fos:examples>
<fos:changes>
<fos:change issue="516" PR="828" date="2023-11-14">
@@ -24771,11 +24735,23 @@ map:for-each($map, fn($key, $value) {

</fos:rules>
<fos:equivalent style="xpath-expression">
<!--
( for $item at $pos in $input
let $val := $value($item, $pos)
for $key in $keys($item, $pos)
return map:pair($key, $val)
) => map:of-pairs($options)
) => map:of-pairs($options) -->

fold-left($input ! {'item': ., 'pos': position()},
{},
fn($map, $pair) {
let $v := $value($pair?item, $pair?pos)
return fold-left($keys($pair?item, $pair>pos), $map, fn($m, $k) {
if (map:contains($m, $k))
then map:put($m, $k, $combine($m($k), $v))
else map:put($m, $k, $v)
})
})
</fos:equivalent>
<fos:errors>

@@ -28325,7 +28301,7 @@ return document {
</fos:rules>
<fos:equivalent style="xpath-expression" covers-error-cases="false">
if ($position = (1 to array:size($array)))
then array:members($array)[$position] => map:get('value')
then items-at(array:members($array), $position) => map:get('value'))
else $fallback($position)
</fos:equivalent>
<fos:errors>
@@ -28742,8 +28718,8 @@ array:index-of(
</fos:rules>

<fos:equivalent style="xpath-expression">
array:fold-left($array, (), fn($indices, $member, $pos) {
$indices, if ($predicate($member, $pos)) { $pos }
dm:iterate-array($array, fn($member, $pos) {
if ($predicate($member, $pos)) { $pos }
})
</fos:equivalent>

@@ -29009,7 +28985,7 @@ $array
<fos:equivalent style="xpath-expression" covers-error-cases="false"><![CDATA[
$array
=> array:members()
=> insert-before($position, map:entry('value', $member ))
=> insert-before($position, array:member($member))
=> array:of-members()
]]></fos:equivalent>
<fos:errors>
@@ -29299,9 +29275,11 @@ $array
integer position.</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
array:fold-left($array, [], fn($zero, $next, $pos) {
array:append($zero, $action($next, $pos))
})
array:of-members(
for-each(array:members($array),
fn($member, $pos) {
array:member( $action($member, $pos) )
}))
</fos:equivalent>
<fos:examples>
<fos:example>
@@ -29365,11 +29343,11 @@ array:fold-left($array, [], fn($zero, $next, $pos) {
array that satisfy the supplied predicate.</p>
</fos:rules>
<fos:equivalent style="xquery-expression">
array:fold-left($array, [], fn($result, $next, $pos) {
if ($predicate($next, $pos))
then array:append($result, $next)
else $result
})
array:of-members(
filter(array:members($array),
fn($item, $pos) {
$predicate(map:get($item, 'value'), $pos
}))
</fos:equivalent>
<fos:errors>
<p>As a consequence of the function signature and the function calling rules, a type error occurs if the supplied
@@ -29444,7 +29422,7 @@ return array:filter(
fold-left(
array:members($array),
$zero,
fn($result, $member, $pos) { $action($result, map:get($member, 'value'), $pos) }
fn($result, $member) { $action($result, map:get($member, 'value')) }
)
]]></fos:equivalent>
<fos:notes>
@@ -29484,34 +29462,20 @@ fold-left(
<fos:result>[[[[], 1], 2], 3]</fos:result>
</fos:test>
</fos:example>
<fos:example>
<fos:test>
<fos:expression><eg>
let $input := array { 11 to 21, 21 to 31 }
let $target := 21
return array:fold-left($input, (),
fn($result, $curr, $pos) {
$result, if ($curr = $target) { $pos }
}
)
</eg></fos:expression>
<fos:result>11, 12</fos:result>
</fos:test>
</fos:example>
</fos:examples>
<fos:changes>
<!--<fos:changes>
<fos:change issue="516" PR="828" date="2023-11-14">
<p>The <code>$action</code> callback function now accepts an optional position argument.</p>
</fos:change>
</fos:changes>
</fos:changes>-->
</fos:function>

<fos:function name="fold-right" prefix="array">
<fos:signatures>
<fos:proto name="fold-right" return-type="item()*">
<fos:arg name="array" type="array(*)" usage="inspection"/>
<fos:arg name="zero" type="item()*" usage="navigation"/>
<fos:arg name="action" type="fn(item()*, item()*, xs:integer) as item()*" usage="inspection"/>
<fos:arg name="action" type="fn(item()*, item()*) as item()*" usage="inspection"/>
</fos:proto>
</fos:signatures>
<fos:properties>
@@ -29532,17 +29496,14 @@ return array:fold-left($input, (),
fold-right(
array:members($array),
$zero,
fn($member, $result, $pos) { $action(map:get($member, 'value'), $result, $pos) }
fn($member, $result) { $action(map:get($member, 'value'), $result) }
)
]]></fos:equivalent>
<fos:notes>
<p>If the supplied array is empty, the function returns <code>$zero</code>.</p>
<p>If the supplied array contains a single member <code>$m</code>, the function returns <code>$action($m, $zero)</code>.</p>
<p>If the supplied array contains two members <code>$m</code> and <code>$n</code>, the function returns
<code>$action($m, $action($n, $zero))</code>; and similarly for an input array with more than two members.</p>
<p>The value of the third argument of <code>$action</code> corresponds to the position
of the member in the input array. Thus, in contrast to <function>array:fold-left</function>,
it is initally set to the number of members in the input array.</p>
</fos:notes>
<fos:examples>
<fos:example>
@@ -29573,20 +29534,6 @@ fold-right(
<fos:result>[ 1, [ 2, [ 3, [] ] ] ]</fos:result>
</fos:test>
</fos:example>
<fos:example>
<fos:test>
<fos:expression><eg>
let $input := array { 11 to 21, 21 to 31 }
let $target := 21
return array:fold-right(
$input, (),
action := fn($curr, $result, $pos) {
$result, if ($curr = $target) { $pos }
}
)</eg></fos:expression>
<fos:result>12, 11</fos:result>
</fos:test>
</fos:example>
</fos:examples>
<fos:changes>
<fos:change issue="516" PR="828" date="2023-11-14">
@@ -29621,9 +29568,13 @@ return array:fold-right(
</fos:rules>
<fos:equivalent style="xpath-expression"><![CDATA[
array:of-members(
for $pos in 1 to min((array:size($array1), array:size($array2)))
return map:entry('value', $action($array1($pos), $array2($pos), $pos))
)
for-each-pair(array:members($array1),
array:members($array2),
fn($v1, $v2, $pos) {
array:member( $action(map:get($v1, 'value'),
map:get($v2, 'value'),
$pos))
}))
]]></fos:equivalent>
<fos:notes>
<p>If the arrays have different size, excess members in the longer array are ignored.</p>
@@ -29693,9 +29644,11 @@ array:for-each-pair(
in the input sequence, and the resulting sequence becomes one member of the returned array.</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, [], fn($array, $next, $pos) {
array:append($array, $action($next, $pos))
})
array:of-members(
for-each($input,
fn($item, $pos) {
array:member($action($item, $pos))
}))
</fos:equivalent>
<fos:notes>
<p>The single-argument function <code>array:build($input)</code> is equivalent to the XPath
@@ -29767,7 +29720,7 @@ array:build(

</fos:rules>
<fos:equivalent style="dm-primitive">
dm:iterate-array($array, map:entry('value', ?))
dm:iterate-array($array, array:member#1)
</fos:equivalent>
<fos:notes>
<p>This function is the inverse of <function>array:of-members</function>.</p>
@@ -29823,8 +29776,7 @@ return deep-equal(
Each returned array encapsulates the value of one member of <code>$array</code>.</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
array:for-each($array, fn($member) { [] => array:append($member) })
=> array:items()
dm:iterate-array($array, fn($member) { array:append([], $member) })
</fos:equivalent>
<fos:notes>
<p>The function call <code>array:split($array)</code> produces the same result as the
@@ -30006,7 +29958,7 @@ $array
$collations,
for $key in ($keys otherwise data#1)
return fn($member as record(value)) as xs:anyAtomicType* {
$key($member?value)
$key(map:get($member, 'value'))
},
$orders
)
@@ -30155,7 +30107,7 @@ declare function array:flatten(

</fos:rules>
<fos:equivalent style="xpath-expression">
for-each(array:members($array), map:get(?, 'value'))
dm:iterate-array($array, fn($member, $pos) { $member } )
</fos:equivalent>
<fos:notes>
<p>Unlike <function>array:flatten</function>, the function does not apply recursively
@@ -31301,9 +31253,8 @@ tail(fold-left(


<fos:equivalent style="xpath-expression">
fold-left($input, true(), fn($result, $item, $pos) {
$result and $predicate($item, $pos)
})
for-each($input, $predicate)
=> fold-left(true(), fn($zero, $item) {$zero and $item})
</fos:equivalent>
<fos:errors>
<p>An error is raised if the <code>$predicate</code> function raises an error. In particular,
@@ -31791,9 +31742,10 @@ fn($item) {

</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, (), fn($indices, $item, $pos) {
$indices, if ($predicate($item, $pos)) { $pos }
})
for-each($input,
fn($item, $pos) {
if ($predicate($item, $pos)) { $pos }
})
</fos:equivalent>

<fos:examples>
@@ -32144,9 +32096,8 @@ return $item
in the input sequence such that <code>$predicate($item, $pos)</code> returns true.</p>
</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, false(), fn($result, $item, $pos) {
$result or $predicate($item, $pos)
})
for-each($input, $predicate)
=> fold-left(false(), fn($zero, $item) {$zero or $item})
</fos:equivalent>
<fos:errors>
<p>An error is raised if the <code>$predicate</code> function raises an error. In particular,
@@ -33484,12 +33435,16 @@ path with an explicit <code>file:</code> scheme.</p>
function returns <code>false</code> or <code>()</code>, the item <var>J</var> is added to the current partition.</p>

</fos:rules>
<fos:equivalent style="xpath-expression">
fold-left($input, (), fn($partitions, $next, $pos) {
if (empty($partitions) or $split-when(foot($partitions)?*, $next, $pos))
then ($partitions, [ $next ])
else (trunk($partitions), array { foot($partitions)?*, $next })
})
<fos:equivalent style="xpath-expression">t
for-each($input, fn($item, $pos) {
map{ 'item': $item, 'pos': $pos }
})
=> fold-left((),
fn($partitions, $pair) {
if (empty($partitions) or $split-when(foot($partitions)?*, $pair?item, $pair?pos))
then ($partitions, [ $pair?item ])
else (trunk($partitions), array { foot($partitions)?*, $pair?item })
})
</fos:equivalent>
<fos:notes>
<p>The function enables a variety of positional grouping problems to be solved. For example:</p>
5 changes: 2 additions & 3 deletions specifications/xpath-functions-40/src/xpath-functions.xml
Original file line number Diff line number Diff line change
@@ -9400,7 +9400,7 @@ return <table>

<div2 id="formal-specification-of-arrays">
<head>Formal Specification of Arrays</head>
<p>The XDM data model (<bibref ref="xpath-datamodel-40"/>) defines three primitive operations on maps:</p>
<p>The XDM data model (<bibref ref="xpath-datamodel-40"/>) defines three primitive operations on arrays:</p>
<ulist>
<item><p><code>dm:empty-array</code> constructs an empty array.</p></item>
<item><p><code>dm:array-append</code> adds a member to an array.</p></item>
@@ -9417,8 +9417,7 @@ return <table>
an empty array, in preference to a call on <code>dm:empty-array()</code>.</p>

<p>The formal equivalents are not intended to provide a realistic way of implementating the
functions (in particular, any real implementation might be expected to implement <function>array:get</function>
much more efficiently). They do, however, provide a framework that allows
functions. They do, however, provide a framework that allows
the correctness of a practical implementation to be verified.</p>