-
Notifications
You must be signed in to change notification settings - Fork 22
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
Adds a PPC for any
and all
as built-in operator syntax
#50
Open
leonerd
wants to merge
3
commits into
Perl:main
Choose a base branch
from
leonerd:any-all
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# More list processing operators inspired by List::Util | ||
|
||
## Preamble | ||
|
||
Author: Paul Evans <PEVANS> | ||
Sponsor: | ||
ID: TODO | ||
Status: Exploratory | ||
|
||
## Abstract | ||
|
||
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`. | ||
|
||
## Motivation | ||
|
||
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. | ||
|
||
## Rationale | ||
|
||
## Specification | ||
|
||
New named features that, when enabled, activate syntax analogous to the existing `grep` operator, named `any` and `all`: | ||
|
||
``` | ||
any { BLOCK } LIST | ||
|
||
all { BLOCK } LIST | ||
``` | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
For example: | ||
|
||
``` | ||
Some::Class->new( | ||
option => (any { TEST } list, of, things), | ||
other => $parameter, | ||
); | ||
``` | ||
|
||
## Backwards Compatibility | ||
|
||
As these new operators are guarded by named features, there are no immediate concerns with backward compatiblity in the short-term. | ||
|
||
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. | ||
|
||
## Security Implications | ||
|
||
## Examples | ||
|
||
``` | ||
use v5.40; | ||
use feature 'any'; | ||
|
||
if( any { $_ > 10 } 5, 10, 15, 20 ) { say "A number above 10" } | ||
``` | ||
|
||
## Prototype Implementation | ||
|
||
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). | ||
|
||
## Future Scope | ||
|
||
### Named Lexicals | ||
|
||
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: | ||
|
||
``` | ||
use List::Keywords qw( any ); | ||
|
||
if( any my $item { we_want($item) } @items ) { | ||
say "There's an item we want here"; | ||
} | ||
``` | ||
|
||
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. | ||
|
||
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. | ||
|
||
### Other Operators | ||
|
||
* The other two variant behaviours of `none` and `notall`. These simply invert the sense of the filter block. | ||
|
||
* Another variation on the theme, `first`. This returns the value from the list itself, that first caused the filter block to be true. | ||
|
||
## Rejected Ideas | ||
|
||
### Block-less syntax | ||
|
||
Supporting syntax analogous to the "deferred-expression" form of `grep EXPR, LIST`. | ||
|
||
### Keywords as Junctions | ||
|
||
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: | ||
|
||
``` | ||
use Data::Checks qw( Any ... ); | ||
use Syntax::Operator::Is; | ||
|
||
if( $x is Any( things... ) ) { ... } | ||
``` | ||
|
||
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. | ||
|
||
## Open Issues | ||
|
||
* 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? | ||
|
||
* Should the flag be called `any`? That might be confusing as compared to the `:any` import tag which would request all features. | ||
|
||
## Copyright | ||
|
||
Copyright (C) 2024, Paul Evans. | ||
|
||
This document and code and documentation within it may be used, redistributed and/or modified under the same terms as Perl itself. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was at first surprised by this behavior, that all would be true on an empty list; though i did see that that's how
every
works in javascript andall
in python, and i suppose it makes sense. I think we could have a word on why this design choice makes sense from a user's perspective rather than just the implementation reason.