Skip to content

Commit 8f45fec

Browse files
authored
Merge pull request #50 from leonerd/any-all
Adds a PPC for `any` and `all` as built-in operator syntax
2 parents 1ac4c20 + 8b42398 commit 8f45fec

File tree

1 file changed

+125
-0
lines changed

1 file changed

+125
-0
lines changed

ppcs/ppcTODO-more-list-utils.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# More list processing operators inspired by List::Util
2+
3+
## Preamble
4+
5+
Author: Paul Evans <PEVANS>
6+
Sponsor:
7+
ID: TODO
8+
Status: Exploratory
9+
10+
## Abstract
11+
12+
Creates several new list-processing operators, similar to the existing `grep`, inspired by the same-named functions in modules like `List::Util` or `List::Keywords`.
13+
14+
## Motivation
15+
16+
Most code of any appreciable size tends to make use of at least the `any` or `all` functions from `List::Util`. Due to limits of their implementation they are not as efficient to call as core's `grep` operator. The implementations provided by `List::Keywords` are more efficient, on the same level as core's `grep`, though being a non-core module it does not appear to be used anywhere near as much in practice.
17+
18+
## Rationale
19+
20+
## Specification
21+
22+
New named features that, when enabled, activate syntax analogous to the existing `grep` operator, named `any` and `all`:
23+
24+
```
25+
any { BLOCK } LIST
26+
27+
all { BLOCK } LIST
28+
```
29+
30+
These operators are similar to `grep` in scalar context, though yield a simple boolean truth value relating to how many input values made the filter block yield true. `any` yields true when its block yields true for at least one of its input values, or false if they all yield false. `all` yields true only if every input value makes the block yield true, or false if at least one yields false.
31+
32+
A consequence of these rules is what happens when given an empty list. A call to `any` with an empty list does not have any input values which made the block return true, so its result is false. Conversely, a call to `all` with an empty list does not have any input values which made the block return false, so its result is true.
33+
34+
The key difference between these operators and `grep` is that these will short-circuit and stop evaluating the block once its result is determined. In particular, the first time `any` sees a true result, it knows its result so it can stop; only testing more input values while each yields false. Conversely, `all` will stop as soon as it sees a false result, knowing that to be its answer; it only continues while each value yields true from the block. This short-circuiting is a key reason to choose these over the `grep` operator.
35+
36+
Additionally, because each operator returns a fixed boolean truth value, the caller does not have to take special precautions against a value that would appear false, which satisfies the filter code block. Such a value would cause the operator to return true, even if the matching value itself appears false.
37+
38+
Like `grep`, each is a true operator, evaluating its block expression without an interposed function call frame. Thus any `caller` or `return` expression or similar within the block will directly affect the function containing the `any` or `all` expression itself.
39+
40+
These operators only yield a single scalar; in list context therefore they will just provide a single-element list containing that boolean scalar. This is so that there are no "surprises" if the operator is used in a list context, such as when building a key/value pair list for the constructor of an object. By returning a single false value even as a list, rather than an empty list, such constructions do no cause issues.
41+
42+
For example:
43+
44+
```
45+
Some::Class->new(
46+
option => (any { TEST } list, of, things),
47+
other => $parameter,
48+
);
49+
```
50+
51+
## Backwards Compatibility
52+
53+
As these new operators are guarded by named features, there are no immediate concerns with backward compatiblity in the short-term.
54+
55+
In the longer term, if these named features become part of a versioned feature bundle that is enabled by a corresponding `use VERSION` declaration there may be concerns that the names collide with functions provided by `List::Util` or similar modules. As the intention of these operators is to provide the same behaviour, this is not considered a major problem. Differences due to caller scope as outlined above may be surprising to a small number of users.
56+
57+
## Security Implications
58+
59+
## Examples
60+
61+
```
62+
use v5.40;
63+
use feature 'any';
64+
65+
if( any { $_ > 10 } 5, 10, 15, 20 ) { say "A number above 10" }
66+
```
67+
68+
## Prototype Implementation
69+
70+
The overall behaviour of these operators is primarily demonstrated by functions from core's existing [`List::Util`](https://metacpan.org/pod/List::Util) module. Additional examples of a more efficient keyword-and-operator implementation can be found in [`List::Keywords`](https://metacpan.org/pod/List::Keywords).
71+
72+
## Future Scope
73+
74+
### Named Lexicals
75+
76+
The `List::Keywords` module also provides an interesting "named lexical" syntax to its operators, allowing the user to specify a lexical variable, rather than the global `$_`, to store each item for iteration:
77+
78+
```
79+
use List::Keywords qw( any );
80+
81+
if( any my $item { we_want($item) } @items ) {
82+
say "There's an item we want here";
83+
}
84+
```
85+
86+
These lexicals are useful when nesting multiple calls to list-processing operators, to avoid collisions in the use of the `$_` global, and lead to cleaner code. They are also useful for suggesting how to support n-at-a-time behaviour of `grep` and `map`-like functions.
87+
88+
If this feature is to be considered, it will require careful thought on how it might interact with the so-far-unspecified idea of accepting `any EXPR, LIST` as `grep` currently does. I would recommend not allowing that variant, to allow for easier implementation of these named lexicals in future as they provide advantages that outweigh the minor inconvenience of having to wrap the expression in brace characters.
89+
90+
### Other Operators
91+
92+
* The other two variant behaviours of `none` and `notall`. These simply invert the sense of the filter block.
93+
94+
* Another variation on the theme, `first`. This returns the value from the list itself, that first caused the filter block to be true.
95+
96+
## Rejected Ideas
97+
98+
### Block-less syntax
99+
100+
Supporting syntax analogous to the "deferred-expression" form of `grep EXPR, LIST`.
101+
102+
### Keywords as Junctions
103+
104+
Using the `any` and `all` keywords to make junction-like behaviour. Such is already provided by other modules, for example [`Data::Checks`](https://metacpan.org/pod/Data::Checks) in a title-case form and thus would not collide with the all-lowercase keywords provided here. This is already possible:
105+
106+
```
107+
use Data::Checks qw( Any ... );
108+
use Syntax::Operator::Is;
109+
110+
if( $x is Any( things... ) ) { ... }
111+
```
112+
113+
In any case, as junctions behave like values, they do not require special syntax like the block-invoking keywords proposed here, so they can be provided by regular function-call syntax from regular modules.
114+
115+
## Open Issues
116+
117+
* There could be anything up to five new operators added by this idea. Do they all get their own named feature flags? Do they all live under one flag?
118+
119+
* Should the flag be called `any`? That might be confusing as compared to the `:any` import tag which would request all features.
120+
121+
## Copyright
122+
123+
Copyright (C) 2024, Paul Evans.
124+
125+
This document and code and documentation within it may be used, redistributed and/or modified under the same terms as Perl itself.

0 commit comments

Comments
 (0)