rfc:any_all_on_iterable

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
rfc:any_all_on_iterable [2020/08/30 16:13] – created tandrerfc:any_all_on_iterable [2021/06/13 14:36] (current) tandre
Line 1: Line 1:
-====== PHP RFC: any() and all() on iterables ====== +====== PHP RFC: PHP\iterable\any() and all() on iterables ====== 
-  * Version: 0.1+  * Version: 0.6
   * Date: 2020-08-30   * Date: 2020-08-30
   * Author: Tyson Andre, tandre@php.net   * Author: Tyson Andre, tandre@php.net
-  * Status: Draft+  * Status: Declined
   * First Published at: https://wiki.php.net/rfc/any_all_on_iterable   * First Published at: https://wiki.php.net/rfc/any_all_on_iterable
   * Implementation: https://github.com/php/php-src/pull/6053   * Implementation: https://github.com/php/php-src/pull/6053
Line 9: Line 9:
 ===== Introduction ===== ===== Introduction =====
  
-The primitives any() and all() are a common part of many programming languages and help in avoiding verbosity or unnecessary abstractions.+The primitives ''any()'' and ''all()'' are a common part of many programming languages and help in avoiding verbosity or unnecessary abstractions.
  
-  - https://hackage.haskell.org/package/base-4.14.0.0/docs/Prelude.html#v:any 
-  - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some 
-  - https://docs.python.org/3/library/functions.html#all 
-  - https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#allMatch-java.util.function.Predicate- 
  
-For example, the following code could be shortened significantly.+  - Haskell: https://hackage.haskell.org/package/base-4.14.0.0/docs/Prelude.html#v:any 
 +  - JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some 
 +  - Python: https://docs.python.org/3/library/functions.html#all 
 +  - Ruby: https://apidock.com/ruby/Enumerable/any%3F 
 +  - Java 8(Stream): https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#allMatch-java.util.function.Predicate- 
 +  - C++: https://www.cplusplus.com/reference/algorithm/any_of/ 
 + 
 +For example, the following code could be shortened significantly:
  
 <code php> <code php>
-// Old version +// The old version 
-$satisifes_predicate = false; +$satisifies_predicate = false; 
-foreach ($items as $item) {+foreach ($item_list as $item) { 
 +    // Performs DB operations or external service requests, stops on first match by design.
     if (API::satisfiesCondition($item)) {     if (API::satisfiesCondition($item)) {
         $satisfies_predicate = true;         $satisfies_predicate = true;
Line 34: Line 38:
  
 <code php> <code php>
-// New version is much shorter, readable, and easier to review, +// The new version is much shorter, readable, and easier to review, 
-// without creating temporary variables or helper functions that are used in only one place +// without creating temporary variables or helper functions that are used in only one place
-if (!any($items, fn($x) => API::satisfiesCondition($x))) {+ 
 +// Performs DB operations or external service requests, stops on first match by design. 
 +if (!\PHP\iterable\any($item_list, fn($item) => API::satisfiesCondition($item))) {
     throw new APIException("No matches found");     throw new APIException("No matches found");
 } }
Line 43: Line 49:
 ===== Proposal ===== ===== Proposal =====
  
-Add the following functions to php's standard function set.+Add the functions ''PHP\iterable\any(iterable $input, ?callable $callback = null): bool'' and ''all(...)'' to PHP's standard library'function set. 
 +(The namespace ''PHP\iterable'' was preferred in [[rfc:any_all_on_iterable_straw_poll_namespace#vote|a straw poll that was previously sent out]]) 
 + 
 +**The implementation is equivalent to the following polyfill:**
  
 <code php> <code php>
-/** Determines whether any element of the iterable satisfies the predicate. */ +namespace PHP\iterable; 
-function any(iterable $input, ?callable $callback = null) {+ 
 +/*
 + * Determines whether any element of the iterable satisfies the predicate. 
 + * 
 + * 
 + * If the value returned by the callback is truthy 
 + * (e.g. true, non-zero number, non-empty array, truthy object, etc.), 
 + * this is treated as satisfying the predicate. 
 + * 
 + * @param iterable $input 
 + * @param null|callable(mixed):mixed $callback 
 + */ 
 +function any(iterable $input, ?callable $callback = null): bool {
     foreach ($input as $v) {     foreach ($input as $v) {
         if ($callback !== null ? $callback($v) : $v) {         if ($callback !== null ? $callback($v) : $v) {
Line 55: Line 76:
     return false;     return false;
 } }
-/** Determines whether all elements of the iterable satisfy the predicate */ +</code> 
-function all(iterable $input, ?callable $callback = null) {+<code php> 
 +/*
 + * Determines whether all elements of the iterable satisfy the predicate
 + * 
 + * If the value returned by the callback is truthy 
 + * (e.g. true, non-zero number, non-empty array, truthy object, etc.), 
 + * this is treated as satisfying the predicate. 
 + * 
 + * @param iterable $input 
 + * @param null|callable(mixed):mixed $callback 
 + */ 
 +function all(iterable $input, ?callable $callback = null): bool {
     foreach ($input as $v) {     foreach ($input as $v) {
         if (!($callback !== null ? $callback($v) : $v)) {         if (!($callback !== null ? $callback($v) : $v)) {
Line 66: Line 98:
 </code> </code>
  
-This proposal recommends adding ''any()'' and ''all()'' to the standard library instead of a PECL or composer library for the following reasons+This proposal recommends adding ''PHP\iterable\any()'' and ''PHP\iterable\all()'' to the standard library instead of a PECL or composer library for the following reasons
  
-  - New contributors to projects wouldn't know about ''any()'' and ''all()'' if it was reimplemented in various composer libraries or util.php files with different semantics/names and only occasionally used.+  - New contributors to projects wouldn't know about ''any()'' and ''all()'' if those functions were reimplemented in various composer libraries or util.php files with different semantics/names and only occasionally used.
   - If this was provided only in userland, there'd be low adoption and code such as the above example (API::somePredicate()) would remain common.   - If this was provided only in userland, there'd be low adoption and code such as the above example (API::somePredicate()) would remain common.
   - If the standard library provided it, then polyfills for newer php functionality could adopt this as well, making cleaner code easier to write.   - If the standard library provided it, then polyfills for newer php functionality could adopt this as well, making cleaner code easier to write.
 +
 +==== Implementation Details ====
 +
 +When ''any()'' or ''all()'' are called with an iterable and a predicate, it internally checks if the value returned by the predicate is truthy (e.g. true, non-zero numbers, non-empty arrays, truthy objects, etc.)
 +
 +When ''any()'' or ''all()'' are called with only an iterable, it is equivalent to checking if any/all of the arguments are truthy. This is equivalent to calling ''any()''/''all()'' with ''fn ($x) => $x'', which is equivalent to calling it with ''fn($x) => (bool)$x''.
 +
 +
 +<code php>
 +php > var_export(PHP\iterable\any([false]));
 +false
 +php > var_export(PHP\iterable\any([true]));
 +true
 +php > var_export(PHP\iterable\any([0]));
 +false
 +php > var_export(PHP\iterable\any([1]));
 +true
 +php > var_export(PHP\iterable\any([0], fn($x) => $x));
 +false
 +php > var_export(PHP\iterable\any([1], fn($x) => $x));
 +true
 +
 +php > var_export(PHP\iterable\all([true, true, true], fn($x) => $x));
 +true
 +php > var_export(PHP\iterable\all([1, 2, 3], fn($x) => $x));
 +true
 +php > var_export(PHP\iterable\all([true, true, false], fn($x) => $x));
 +false
 +php > var_export(PHP\iterable\all([1, 2, 0], fn($x) => $x));
 +false
 +php > var_export(PHP\iterable\all([1, 2, 0]);
 +false
 +</code>
 +
 +==== Secondary Vote: any()/all() or any_value()/all_values() ====
 +
 +A secondary vote will be held on whether to name this ''any()''/''all()'' or ''any_value()''/''all_values()''
 +
 +PHP is unique in that the primitive array-like type ''array'' type is also a dictionary, making the keys often significant (strings, numeric identifiers, etc).
 +Existing function names vary in whether the fact that they only act on values is explicitly included in the name.
 +
 +Many other programming languages have gone with a short name for the default of checking if a value is in a collection.
 +
 +<blockquote>
 +The primitives ''any()'' and ''all()'' are a common part of many programming languages and help in avoiding verbosity or unnecessary abstractions.
 +
 +  - Haskell: https://hackage.haskell.org/package/base-4.14.0.0/docs/Prelude.html#v:any
 +  - JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
 +  - Python: https://docs.python.org/3/library/functions.html#all
 +  - Ruby: https://apidock.com/ruby/Enumerable/any%3F
 +  - Java 8(Stream): https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#allMatch-java.util.function.Predicate-
 +  - C++: https://www.cplusplus.com/reference/algorithm/any_of/
 +</blockquote>
 +
 +Benefits of a shorter name:
 +
 +  - Conciseness for the most common use case of checking whether a predicate is true for any/all values of an iterable array/object.
 +  - Consistency with some other functions such as ''array_reduce()'', ''array_unique()'', ''in_array()'', ''next()'' that use values for their underlying implementation (i.e. not being named ''array_reduce_values()'', ''next_value()'', etc.)
 +  - Potential to use $flags to extend this to support less common use cases like ''ARRAY_FILTER_USE_KEY''/''ARRAY_FILTER_USE_BOTH'' without adding more global functions
 +
 +Benefits of a longer name:
 +
 +  - A longer name would be more descriptive and make it easier to understand what code is doing.
 +  - This makes it likely that in the future for iterable functionality, PHP will add multiple functions such as ''any_key()''/(''any_entry'' or ''any_key_value'') instead of using $flags (which will be simpler to statically analyze or infer types for - in rare cases the argument $flags passed to ''array_filter($values, $callback, $flags)'' is an unknown dynamic value). \\ Adding a constant such as ''flags: PHP\iterable\USE_KEY'' may make the code longer. \\ Note that adding the name ''any()'' for values of iterables doesn't prevent PHP from adding ''any_key()'' for checking keys of iterables in the future, either (my personal preference would be to add ''any_key()'' regardless of whether ''any()''/''any_value()'' was added).
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
  
-Any userland functions called ''any()'' and ''all()'' in the global namespace would cause duplicate function errors+Any userland functions called ''PHP\iterable\any()'' and ''PHP\iterable\all()'' in the global namespace without a ''!function_exists()'' check would encounter duplicate function errors
 +Because the [[https://www.php.net/manual/en/language.namespaces.rationale.php|PHP namespace is reserved for internal use by PHP]], this is unlikely.
  
 ===== Proposed PHP Version(s) ===== ===== Proposed PHP Version(s) =====
Line 102: Line 199:
 </blockquote> </blockquote>
  
-===== Proposed Voting Choices =====+==== Add first($iterable, $callback null, $default null): mixed as well? ====
  
-Add ''any(iterable $input, ?callable $callback = null)'' and ''all(...)'' (yes/no, requiring 2/3 majority)+https://externals.io/message/111711#111732
  
-===== References =====+<blockquote> 
 +If it took the default value as well it could return that (to distinguish the absence of a result from null matching the predicate). While it's 
 +useful in itself it also would enable you to pass a marker object and check 
 +the identity of that to know if no matches have been found:
  
-- https://externals.io/message/111711 "Proposal: Adding functions any(iterable $input, ?callable $cb nullint $use_flags=0and all(...)" +<code php> 
-- https://externals.io/message/103357+$none = new stdClass; 
 +$element first($collectionfn($elt=> ..., $none); 
 +if ($element === $none{ 
 +    // nothing found 
 +
 +</code> 
 +</blockquote>
  
 +Calling it ''[iterable_]search_callback()'' or ''first_match[ing]()'' or ''find()'' might help distinguish this from the ''reset()''/''end()''/''next()''/''prev()'' family of global functions - there's more than one possible name.
 +
 +===== Discussion =====
 +
 +==== Alternative names ====
 +
 +''any_value()'' or ''all_values()'' have been suggested as alternative names: https://github.com/php/php-src/pull/6053#issuecomment-684164832
 +
 +<blockquote>
 +I suggest slightly different signatures, assuming we stay value-oriented:
 +
 +<code php>
 +// ...omitted
 +
 +// with named parameters
 +all_values(of: [1, 3, 5, 7], satisfy: 'is_odd');
 +any_value(of: [0, 2, 4, 6], satisfies: 'is_prime');
 +
 +// without named parameters
 +all_values([1, 3, 5, 7], 'is_odd');
 +any_value([0, 2, 4, 6], 'is_prime');
 +</code>
 +The naming clarifies what any and all are about--the values--and leaves room for naming functions that are key or key/value oriented.
 +</blockquote>
 +
 +''iter_any()'' or ''iterable_any()'' have also been suggested as alternative names.
 +
 +<blockquote>
 +The main thing I'm concerned about is that once we start extending this
 +area (I assume that any & all are not going to be the last additions in
 +this space) we will quickly run into function names that are either too
 +generic or outright collide. For example, what if we want to add an
 +iterator-based version of range()? Do we really want to be forced to pull
 +a Python and call it xrange()? That's about as good as real_range()...
 +
 +As such, I think it's important to prefix these somehow, though I don't
 +care strongly how. Could be iter_all() or iterable_all(). We might even
 +make it iterator_all() if we also adjust other existing iterator_*
 +functions to accept iterables. I'd also be happy with iter\all() or
 +iterable\all(), but that gets us back into namespacing discussions :)
 +</blockquote>
 +
 +Because ''any()'' and ''all()'' are potentially commonly used functions in the same way as ''count(Countable|array)'' and always return booleans, I preferred a short name over longer names.
 +This also allows potentially supporting ''int $flags = 0'' in the future, similar to what was done for ''array_filter()''.
 +
 +Initially, the proposal was to add this in the global scope as ''iterable_all()'' and ''iterable_any()''.
 +
 +==== Add find_first() instead? ====
 +
 +<blockquote>
 +I was actually working on this sort of thing recently. Technically,
 +you can support all, any, and first by using a single function:
 +
 +<code php>
 +function find_first(iterable $of, callable($value, $key): bool $thatSatistifes): Iterator
 +</code>
 +
 +It converts the ''$iterable'' into an ''Iterator'', then calls the callback
 +for each key/value pair until one returns true, and then always
 +returns the iterator at the current position.
 +
 +This allows you to know both key and value when making a decision.
 +By returning an iterator the caller can get both key and value.
 +By returning an iterator it can handle both the empty case and not
 +found cases with ''$result->valid() === false''.
 +By returning an iterator it might be useful for processing the
 +remainder of the list somehow.
 +I'm not sure that in practice it would be that friendly, but it's
 +worth pointing out for discussion at least.
 +</blockquote>
 +
 +
 +===== Vote =====
 +
 +Add ''PHP\iterable\any(iterable $input, ?callable $callback = null): bool'' and ''PHP\iterable\all(iterable $input, ?callable $callback = null): bool'' (yes/no, requiring a 2/3 majority)
 +
 +Voting started on 2021-02-08 and ended on 2021-02-22.
 +
 +<doodle title="Add PHP\iterable\any() and all() to PHP?" voteType="single" auth="tandre" closed="true">
 +   * Yes
 +   * No
 +</doodle>
 +
 + \\ The following secondary vote will be used to decide between ''any()''/''all()'' and ''any_value()''/''all_values()'' as the name within the ''PHP\iterable'' namespace. See [[#secondary_voteanyall_or_any_valueall_values|the discussion section for the benefits/drawbacks of those names]].
 +
 +<doodle title="Names to use: any()/all() or any_value()/all_values()" voteType="single" auth="tandre" closed="false">
 +   * any()/all()
 +   * any_value()/all_values()
 +</doodle>
 +
 +==== Straw Poll ====
 +
 +<doodle title="Reasons for voting against this RFC" voteType="multi" auth="tandre" closed="false">
 +   * Too small in scope
 +   * Object to the choice of namespace
 +   * Prefer the global namespace
 +   * Confused about the implementation
 +   * Prefer userland solutions
 +   * Other
 +   * Voted for this RFC
 +</doodle>
 +
 +===== References =====
 +
 +  - https://externals.io/message/111711 "Proposal: Adding functions any(iterable $input, ?callable $cb = null, int $use_flags=0) and all(...)"
 +  - https://externals.io/message/103357 "[PATCH] Implementing array_every() and array_any()"
 +  - https://externals.io/message/111756 "[RFC] Global functions any() and all() on iterables"
 +  - [[rfc:any_all_on_iterable_straw_poll|Straw poll: Naming for *any() and *all() on iterables]]
 +  - [[rfc:any_all_on_iterable_straw_poll_namespace|Straw poll: Using namespaces for *any() and *all() on iterables]] (older poll)
 +  - [[rfc:namespaces_in_bundled_extensions|RFC: Namespaces in bundled PHP extensions]] (started after voting closed)
 +  - [[rfc:cachediterable_straw_poll|Straw poll: Namespace to use for CachedIterable and iterable functionality]] (newer poll)
 ===== Rejected Features ===== ===== Rejected Features =====
-Keep this updated with features that were discussed on the mail lists.+ 
 +Adding flags like [[https://php.net/array_filter|''array_filter()'']] was left out of this RFC due to debate over how often it would be used in practice and moved to future scope. 
 + 
 +===== Changelog ====== 
 + 
 +  * 0.3: Add more quotes 
 +  * 0.4: Change name to ''PHP\iterable\all'' and ''PHP\iterable\any'', add a secondary vote on ''any/all'' vs ''any_value()/all_values()'' 
 +  * 0.5: Add straw poll 
 +  * 0.6: Add examples of how this works, add in missing return type, clarify treatment of predicate $callback return type
rfc/any_all_on_iterable.1598803986.txt.gz · Last modified: 2020/08/30 16:13 by tandre