From c27e326d59884993560acfaf1c5a8f28153f2c07 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 27 Oct 2026 23:12:05 +0100 Subject: [PATCH] document callback adaptation, errors and min/max ties --- README.md | 58 ++++++++++++++++++++++++------- src/Compilable.php | 7 ++-- tests/Collections/QueriesTest.php | 11 ++++++ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5889743..e0bf45e 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,10 @@ $slow('stogi'); // remembered ## Compiling decorators -`partial()`, `curry()` and `fix()` return decorator objects. Calling one goes through its `__invoke()`, which -inspects the arguments every time. That is fine for a single call, but in a loop over thousands of elements it -adds up. `f\compile()` flattens such a callable into a plain `\Closure` once, so the loop pays only for the -function itself: +`partial()`, `curry()`, `fix()`, `pipe()` and the predicates return objects. Calling one goes through its +`__invoke()`, which inspects the arguments every time. That is fine for a single call, but in a loop over thousands +of elements it adds up. `f\compile()` flattens such a callable into a plain `\Closure` once, so the loop pays only +for the functions it calls: ```php use Kadet\Functional as f; @@ -139,9 +139,17 @@ $pad = f\compile(f\partial(str_pad(...), _, 3, '0', STR_PAD_LEFT)); $pad('7'); // '007' ``` -Objects implementing `Kadet\Functional\Compilable` are compiled with their own `compile()`; any other callable is -converted to a closure. Implement `Compilable` on your own decorators to get the same benefit. -You rarely need to call `compile()` yourself: the collection functions compile every callback once per step. +Objects implementing `Kadet\Functional\Compilable` are compiled with their own `compile()`, also when given as a +closure of their `__invoke()`; any other callable is converted to a closure. Compiling is recursive: a predicate +like `p\property('delay', p\gt(60))` becomes a closure calling the accessor and the comparison directly. Implement +`Compilable` on your own decorators to get the same benefit. `memoize()` is not compiled, its cache is the point. + +A compiled callable behaves like the object, with two differences. When the call ends in an internal function, +arguments it can't take are dropped instead of throwing. And a compiled `curry()` calls its function with the +arguments it gets, without currying further. + +You rarely need to call `compile()` yourself: the collection functions compile every callback once per step, +predicates and pipes included. ## Collections @@ -161,9 +169,10 @@ $lines = $departures ``` The namespace says which flavour you get. `c\` is eager and gives a finished collection after every step. `lazy\` -holds the streaming transforms, which give a generator and do no work until something reads it. Sorting, grouping, -collectors and queries need the whole input, so they exist only in `c\`. They take any iterable, generators -included, and are how a lazy chain ends. +holds the streaming transforms, which give a generator and do no work until something reads it. Collectors and +queries end a chain, turning it into a collection or a single value, so there is nothing to delay and they exist +only in `c\`. They take any iterable, generators included, and are how a lazy chain ends. Sorting needs the whole +input before it can give the first element, so it is eager only as well. ### Pipes, and calling without them @@ -206,12 +215,19 @@ argument, so a naive `($value, $key)` call would round the first element to 0 pl third to 2. To prevent that, two kinds of callables are called with the value only: - **PHP's internal functions**, written as first-class callables: `trim(...)`, `round(...)`, `strtoupper(...)`. - This also holds when they are wrapped, e.g. `f\partial(str_pad(...), _, 3, '0')`. - **Any callable declaring exactly one parameter**, like `fn ($value) => ...` or a one-parameter method. +The library's own wrappers are seen through: what counts is the function a callable ultimately calls. For +`partial()`, `curry()`, `memoize()` and `f\predicate()` that is the wrapped function, for `f\not()` the +negated predicate, and for `f\pipe()` its first function, the one that gets the arguments. So +`f\partial(str_pad(...), _, 3, '0')`, `f\memoize(fn ($stop) => ...)` (cached by the value alone) and +`f\pipe(trim(...), strtolower(...))` all get only the value. `f\all()` and `f\any()` decide for each of their +predicates: in `f\all(is_numeric(...), fn ($value, $key) => ...)` the first gets the value, the second the key too. + ```php [1.25, 2.5, 3.14159] |> c\map(round(...)); // [1.0, 3.0, 3.0], not [1.0, 2.5, 3.14] [' a ', ' b'] |> c\map(trim(...)); // ['a', 'b']; the key never becomes trim's character list +['n' => ' Main St '] |> c\map(f\pipe(trim(...), strtolower(...))); // ['n' => 'main st'] ``` Any other callable gets the key, **even into an optional parameter**. A method like @@ -316,7 +332,7 @@ $byId->get('d4')->destination; // 'Oliwa' ``` `toArray()` throws an `InvalidArgumentException` when a key can't be an array key, like an object key of a -`Ds\Map`, instead of PHP's "Illegal offset type". +`Ds\Map`, instead of PHP's "Illegal offset type". So does `merge()` when it merges such keys into an array. ### Queries @@ -341,7 +357,23 @@ the largest delay, not the departure that has it. To get the element, sort and t $departures |> c\sortBy(t\property('delay'), descending: true) |> c\first(); // d4 ``` -`reduce()` callbacks take the carry first: `fn ($carry, $value, $key)`. +On a tie, `min()` and `max()` return the first of the equal values: `[1, 1.0] |> c\max()` is `1`, and of two +objects equal under `<=>` you get the one that came first. + +`reduce()` callbacks take the carry first: `fn ($carry, $value, $key)`. Like the value-only rule above, an internal +function or a callable declaring exactly two parameters gets only `($carry, $value)`, so +`[5 => 1, 9 => 2] |> c\reduce(max(...), 0)` is `2`, not the key `9`. + +### Errors + +Exceptions name the function and the problem. Two kinds are thrown: + +- **Invalid arguments** raise PHP's own SPL exceptions: `InvalidArgumentException` (`c\take(-1)`, a key that can't be + an array key in `toArray()` or `merge()`) and `UnexpectedValueException` (a `flatMap()` callback that returns no + iterable). +- **Library conditions** raise subclasses of `Kadet\Functional\Exception\FunctionalException`: + `EmptyCollectionException` (a `required` query found nothing) and `DsUnavailableException` (a collector needs + php-ds, see below). ### Lazy pipelines diff --git a/src/Compilable.php b/src/Compilable.php index 74a50a6..753b267 100644 --- a/src/Compilable.php +++ b/src/Compilable.php @@ -17,8 +17,11 @@ interface Compilable /** * Returns a closure that behaves like invoking this object, but does not go through it on each call. * - * The one difference: when the call ends in a non-variadic internal function, arguments past its parameter count - * are dropped, where invoking this object would throw an ArgumentCountError. + * The differences: + * - when the call ends in a non-variadic internal function, arguments past its parameter count are dropped, where + * invoking this object would throw an ArgumentCountError (`all()` and `any()` likewise give each predicate only + * the arguments it takes); + * - a compiled CurriedFunction calls its function with the arguments it gets, it does not curry further. */ public function compile(): \Closure; } diff --git a/tests/Collections/QueriesTest.php b/tests/Collections/QueriesTest.php index fb4cc42..3779b77 100644 --- a/tests/Collections/QueriesTest.php +++ b/tests/Collections/QueriesTest.php @@ -192,6 +192,17 @@ class QueriesTest extends \PHPUnit\Framework\TestCase $this->assertNull(c\max()([])); } + public function testMinMaxTiesKeepFirstExtremum() + { + $a = (object) ['x' => 1]; + $b = (object) ['x' => 1]; + + $this->assertSame($a, c\min()([$a, $b])); + $this->assertSame($a, c\max()([$a, $b])); + $this->assertSame(1, c\max()([1, 1.0])); + $this->assertSame(1.0, c\min()([1.0, 1])); + } + public function testMinWithSelector() { $this->assertSame(1, c\min(fn ($v) => strlen($v))(['ccc', 'a', 'bb']));