compile one-argument pipelines and predicates without spreading

This commit is contained in:
Kacper Donat 2026-10-27 23:09:23 +01:00
parent f3f05fd00c
commit 04dfb21916
7 changed files with 81 additions and 8 deletions

View File

@ -111,3 +111,20 @@ function target(callable $callable): callable
return $callable;
}
}
/**
* Whether calling a closure with one argument is the same as calling it with more: a userland, non-variadic closure
* with one required parameter, as PHP drops the arguments past it. A closure wrapping it can then take one argument
* as well, which is cheaper to call than a variadic one.
*
* @internal
*/
function isUnary(\Closure $closure): bool
{
$reflection = new \ReflectionFunction($closure);
return !$reflection->isInternal()
&& !$reflection->isVariadic()
&& $reflection->getNumberOfParameters() === 1
&& $reflection->getNumberOfRequiredParameters() === 1;
}

View File

@ -14,6 +14,7 @@ use Kadet\Functional\Compilable;
use Kadet\Functional\Predicate;
use function Kadet\Functional\Internal\callback;
use function Kadet\Functional\Internal\isUnary;
class AllOfPredicate extends AbstractPredicate implements Compilable
{
@ -53,6 +54,18 @@ class AllOfPredicate extends AbstractPredicate implements Compilable
{
$predicates = array_map(static fn (Predicate $predicate) => callback($predicate), $this->predicates);
if ($predicates && count(array_filter($predicates, isUnary(...))) === count($predicates)) {
return static function ($value) use ($predicates): bool {
foreach ($predicates as $predicate) {
if (!$predicate($value)) {
return false;
}
}
return true;
};
}
return static function (mixed ...$args) use ($predicates): bool {
foreach ($predicates as $predicate) {
if (!$predicate(...$args)) {

View File

@ -14,6 +14,7 @@ use Kadet\Functional\Compilable;
use Kadet\Functional\Predicate;
use function Kadet\Functional\Internal\callback;
use function Kadet\Functional\Internal\isUnary;
class AnyOfPredicate extends AbstractPredicate implements Compilable
{
@ -54,6 +55,18 @@ class AnyOfPredicate extends AbstractPredicate implements Compilable
{
$predicates = array_map(static fn (Predicate $predicate) => callback($predicate), $this->predicates);
if ($predicates && count(array_filter($predicates, isUnary(...))) === count($predicates)) {
return static function ($value) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate($value)) {
return true;
}
}
return false;
};
}
return static function (mixed ...$args) use ($predicates): bool {
foreach ($predicates as $predicate) {
if ($predicate(...$args)) {

View File

@ -14,6 +14,7 @@ use Kadet\Functional\Compilable;
use Kadet\Functional\Decorator;
use function Kadet\Functional\compile;
use function Kadet\Functional\Internal\isUnary;
class ClosurePredicate extends AbstractPredicate implements Decorator, Compilable
{
@ -54,6 +55,8 @@ class ClosurePredicate extends AbstractPredicate implements Decorator, Compilabl
return $compiled;
}
return static fn (mixed ...$args): bool => $compiled(...$args);
return isUnary($compiled)
? static fn ($value): bool => $compiled($value)
: static fn (mixed ...$args): bool => $compiled(...$args);
}
}

View File

@ -15,6 +15,7 @@ use Kadet\Functional\Decorator;
use Kadet\Functional\Predicate;
use function Kadet\Functional\compile;
use function Kadet\Functional\Internal\isUnary;
class NegatedPredicate extends AbstractPredicate implements Decorator, Compilable
{
@ -49,6 +50,8 @@ class NegatedPredicate extends AbstractPredicate implements Decorator, Compilabl
{
$negated = compile($this->negated);
return static fn (mixed ...$args): bool => !$negated(...$args);
return isUnary($negated)
? static fn ($value): bool => !$negated($value)
: static fn (mixed ...$args): bool => !$negated(...$args);
}
}

View File

@ -12,6 +12,7 @@ namespace Kadet\Functional\Utils;
use Kadet\Functional\Compilable;
use function Kadet\Functional\compile;
use function Kadet\Functional\Internal\isUnary;
class FunctionPipeline implements Compilable
{
@ -82,13 +83,15 @@ class FunctionPipeline implements Compilable
return $first;
}
if (count($functions) === 1) {
$next = $functions[0];
// A first function taking one argument ignores any other, so the compiled pipeline can take one as well.
if (isUnary($first)) {
return static function ($value) use ($first, $functions) {
$result = $first($value);
foreach ($functions as $function) {
$result = $result instanceof UnpackedArgs ? $function(...$result->getArrayCopy()) : $function($result);
}
return static function (...$args) use ($first, $next) {
$result = $first(...$args);
return $result instanceof UnpackedArgs ? $next(...$result->getArrayCopy()) : $next($result);
return $result;
};
}

View File

@ -292,4 +292,25 @@ class CompileTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($compiled(2));
$this->assertSame(0, $inner->calls);
}
public function testUnaryCompiledCallablesMatchWithExtraArguments()
{
$callables = [
f\pipe(fn ($x) => $x, strtoupper(...)),
f\predicate(fn ($x) => $x === 'a'),
f\not(fn ($x) => $x === 'a'),
f\all(fn ($x) => $x === 'a', p\same('a')),
f\any(fn ($x) => $x === 'b', p\same('a')),
];
foreach ($callables as $callable) {
$this->assertSame($callable('a', 'extra'), f\compile($callable)('a', 'extra'));
}
}
public function testCompiledEmptyCombinatorsTakeNoArguments()
{
$this->assertSame(f\all()(), f\compile(f\all())());
$this->assertSame(f\any()(), f\compile(f\any())());
}
}