compile predicates and pipelines
This commit is contained in:
parent
eac27c9e63
commit
0c91360368
@ -10,9 +10,12 @@
|
||||
namespace Kadet\Functional\Predicate;
|
||||
|
||||
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Predicate;
|
||||
|
||||
class AllOfPredicate extends AbstractPredicate
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
class AllOfPredicate extends AbstractPredicate implements Compilable
|
||||
{
|
||||
/**
|
||||
* Array of predicates to check if all are satisfied.
|
||||
@ -41,4 +44,19 @@ class AllOfPredicate extends AbstractPredicate
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public function compile(): \Closure
|
||||
{
|
||||
$predicates = array_map(compile(...), $this->predicates);
|
||||
|
||||
return static function (mixed ...$args) use ($predicates): bool {
|
||||
foreach ($predicates as $predicate) {
|
||||
if (!$predicate(...$args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
namespace Kadet\Functional\Predicate;
|
||||
|
||||
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Predicate;
|
||||
|
||||
class AnyOfPredicate extends AbstractPredicate
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
class AnyOfPredicate extends AbstractPredicate implements Compilable
|
||||
{
|
||||
/**
|
||||
* Array of predicates to check if all are satisfied.
|
||||
@ -42,4 +45,19 @@ class AnyOfPredicate extends AbstractPredicate
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function compile(): \Closure
|
||||
{
|
||||
$predicates = array_map(compile(...), $this->predicates);
|
||||
|
||||
return static function (mixed ...$args) use ($predicates): bool {
|
||||
foreach ($predicates as $predicate) {
|
||||
if ($predicate(...$args)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
namespace Kadet\Functional\Predicate;
|
||||
|
||||
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Decorator;
|
||||
|
||||
class ClosurePredicate extends AbstractPredicate implements Decorator
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
class ClosurePredicate extends AbstractPredicate implements Decorator, Compilable
|
||||
{
|
||||
/**
|
||||
* @var \Closure
|
||||
@ -38,4 +41,19 @@ class ClosurePredicate extends AbstractPredicate implements Decorator
|
||||
{
|
||||
return $this->closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* A closure already declared to return bool is returned as it is (compiled, if it wraps a Compilable).
|
||||
*/
|
||||
public function compile(): \Closure
|
||||
{
|
||||
$compiled = compile($this->closure);
|
||||
$type = (new \ReflectionFunction($compiled))->getReturnType();
|
||||
|
||||
if ($type instanceof \ReflectionNamedType && $type->getName() === 'bool' && !$type->allowsNull()) {
|
||||
return $compiled;
|
||||
}
|
||||
|
||||
return static fn (mixed ...$args): bool => $compiled(...$args);
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,10 @@ namespace Kadet\Functional\Predicate;
|
||||
|
||||
|
||||
use function Kadet\Functional\{ all, any };
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Predicate;
|
||||
|
||||
class ConstantPredicate extends AbstractPredicate
|
||||
class ConstantPredicate extends AbstractPredicate implements Compilable
|
||||
{
|
||||
/** @var boolean */
|
||||
private $value;
|
||||
@ -58,6 +59,13 @@ class ConstantPredicate extends AbstractPredicate
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function compile(): \Closure
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
return static fn (mixed ...$args): bool => $value;
|
||||
}
|
||||
|
||||
public static function always()
|
||||
{
|
||||
static $always;
|
||||
|
||||
@ -10,10 +10,13 @@
|
||||
namespace Kadet\Functional\Predicate;
|
||||
|
||||
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Decorator;
|
||||
use Kadet\Functional\Predicate;
|
||||
|
||||
class NegatedPredicate extends AbstractPredicate implements Decorator
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
class NegatedPredicate extends AbstractPredicate implements Decorator, Compilable
|
||||
{
|
||||
/**
|
||||
* Predicate to be negated.
|
||||
@ -41,4 +44,11 @@ class NegatedPredicate extends AbstractPredicate implements Decorator
|
||||
{
|
||||
return $this->negated;
|
||||
}
|
||||
|
||||
public function compile(): \Closure
|
||||
{
|
||||
$negated = compile($this->negated);
|
||||
|
||||
return static fn (mixed ...$args): bool => !$negated(...$args);
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,7 @@ use function Kadet\Functional\predicate;
|
||||
*/
|
||||
function equals($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $expected == $value;
|
||||
});
|
||||
}
|
||||
@ -31,7 +31,7 @@ function equals($expected): Predicate
|
||||
*/
|
||||
function same($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $value === $expected;
|
||||
});
|
||||
}
|
||||
@ -43,7 +43,7 @@ function same($expected): Predicate
|
||||
*/
|
||||
function gt($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $value > $expected;
|
||||
});
|
||||
}
|
||||
@ -55,7 +55,7 @@ function gt($expected): Predicate
|
||||
*/
|
||||
function ge($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $value >= $expected;
|
||||
});
|
||||
}
|
||||
@ -67,7 +67,7 @@ function ge($expected): Predicate
|
||||
*/
|
||||
function lt($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $value < $expected;
|
||||
});
|
||||
}
|
||||
@ -79,7 +79,7 @@ function lt($expected): Predicate
|
||||
*/
|
||||
function le($expected): Predicate
|
||||
{
|
||||
return predicate(function ($value) use ($expected) {
|
||||
return predicate(function ($value) use ($expected): bool {
|
||||
return $value <= $expected;
|
||||
});
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ function field(string $field, $predicate)
|
||||
*/
|
||||
function instance(string $class)
|
||||
{
|
||||
return new ClosurePredicate(function ($value) use ($class) {
|
||||
return new ClosurePredicate(function ($value) use ($class): bool {
|
||||
return $value instanceof $class;
|
||||
});
|
||||
}
|
||||
@ -9,8 +9,11 @@
|
||||
|
||||
namespace Kadet\Functional\Utils;
|
||||
|
||||
use Kadet\Functional\Compilable;
|
||||
|
||||
class FunctionPipeline
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
class FunctionPipeline implements Compilable
|
||||
{
|
||||
/**
|
||||
* @var callable[]
|
||||
@ -45,4 +48,47 @@ class FunctionPipeline
|
||||
: $callable($arg);
|
||||
}, new UnpackedArgs($args));
|
||||
}
|
||||
|
||||
/**
|
||||
* The first function gets all arguments, each next one the result of the previous one (spread when it is
|
||||
* UnpackedArgs), like invoking the pipeline.
|
||||
*/
|
||||
public function compile(): \Closure
|
||||
{
|
||||
if (empty($this->functions)) {
|
||||
return static fn (...$args) => new UnpackedArgs($args);
|
||||
}
|
||||
|
||||
// A curried function decides on its own whether to call or keep currying, so it is called as is.
|
||||
$functions = array_map(
|
||||
static fn (callable $function) => $function instanceof CurriedFunction
|
||||
? static fn (...$args) => $function(...$args)
|
||||
: compile($function),
|
||||
$this->functions,
|
||||
);
|
||||
$first = array_shift($functions);
|
||||
|
||||
if (empty($functions)) {
|
||||
return $first;
|
||||
}
|
||||
|
||||
if (count($functions) === 1) {
|
||||
$next = $functions[0];
|
||||
|
||||
return static function (...$args) use ($first, $next) {
|
||||
$result = $first(...$args);
|
||||
|
||||
return $result instanceof UnpackedArgs ? $next(...$result->getArrayCopy()) : $next($result);
|
||||
};
|
||||
}
|
||||
|
||||
return static function (...$args) use ($first, $functions) {
|
||||
$result = $first(...$args);
|
||||
foreach ($functions as $function) {
|
||||
$result = $result instanceof UnpackedArgs ? $function(...$result->getArrayCopy()) : $function($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -68,10 +68,18 @@ function memoize(callable $callable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens a callable into a plain closure. Compilable decorators are compiled, so calling the result does not go
|
||||
* through them; any other callable is converted with closure().
|
||||
* Flattens a callable into a plain closure. Compilable objects (decorators, pipelines, predicates) are compiled, so
|
||||
* calling the result does not go through them, also when given as a closure of their `__invoke` (e.g. `$pipe(...)`);
|
||||
* any other callable is converted with closure().
|
||||
*/
|
||||
function compile(callable $callable): \Closure
|
||||
{
|
||||
if ($callable instanceof \Closure) {
|
||||
$reflection = new \ReflectionFunction($callable);
|
||||
$bound = $reflection->getClosureThis();
|
||||
|
||||
return $bound instanceof Compilable && $reflection->getName() === '__invoke' ? $bound->compile() : $callable;
|
||||
}
|
||||
|
||||
return $callable instanceof Compilable ? $callable->compile() : closure($callable);
|
||||
}
|
||||
@ -10,7 +10,11 @@
|
||||
use Kadet\Functional as f;
|
||||
use Kadet\Functional\Compilable;
|
||||
use Kadet\Functional\Decorator;
|
||||
use Kadet\Functional\Predicate\ClosurePredicate;
|
||||
use Kadet\Functional\Predicates as p;
|
||||
use Kadet\Functional\Utils\FunctionPipeline;
|
||||
use Kadet\Functional\Utils\PartiallyAppliedFunction;
|
||||
use Kadet\Functional\Utils\UnpackedArgs;
|
||||
use const Kadet\Functional\_;
|
||||
|
||||
class CompileTest extends \PHPUnit\Framework\TestCase
|
||||
@ -177,4 +181,115 @@ class CompileTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertSame(6, $compiled(3));
|
||||
$this->assertSame(24, $compiled(4));
|
||||
}
|
||||
|
||||
public function testPipelineAndPredicatesAreCompilable()
|
||||
{
|
||||
$this->assertInstanceOf(Compilable::class, f\pipe('strtoupper'));
|
||||
$this->assertInstanceOf(Compilable::class, f\predicate(fn ($x) => $x));
|
||||
$this->assertInstanceOf(Compilable::class, f\not(fn ($x) => $x));
|
||||
$this->assertInstanceOf(Compilable::class, f\all(fn ($x) => $x));
|
||||
$this->assertInstanceOf(Compilable::class, f\any(fn ($x) => $x));
|
||||
$this->assertInstanceOf(Compilable::class, f\always());
|
||||
}
|
||||
|
||||
public function testCompiledPipelineDoesNotInvokePipeline()
|
||||
{
|
||||
$spy = new class(trim(...), strtoupper(...)) extends FunctionPipeline {
|
||||
public int $calls = 0;
|
||||
|
||||
public function __invoke(...$args)
|
||||
{
|
||||
$this->calls++;
|
||||
return parent::__invoke(...$args);
|
||||
}
|
||||
};
|
||||
|
||||
$compiled = $spy->compile();
|
||||
|
||||
$this->assertSame('A', $compiled(' a '));
|
||||
$this->assertSame(0, $spy->calls);
|
||||
}
|
||||
|
||||
public function testCompiledPipelineMatchesPipeline()
|
||||
{
|
||||
$pipelines = [
|
||||
f\pipe(),
|
||||
f\pipe(fn (...$args) => $args),
|
||||
f\pipe(trim(...), strtoupper(...)),
|
||||
f\pipe(fn ($a, $b) => $a + $b, fn ($x) => $x * 2, fn ($x) => $x - 1),
|
||||
f\pipe(fn ($a, $b) => new UnpackedArgs([$b, $a]), fn ($a, $b) => "$a$b"),
|
||||
];
|
||||
|
||||
foreach ($pipelines as $pipeline) {
|
||||
$this->assertEquals($pipeline(1, 2), f\compile($pipeline)(1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCompiledPipelineKeepsCurrying()
|
||||
{
|
||||
$pipeline = f\pipe(strtoupper(...), f\curry(fn ($a, $b) => "$a$b"));
|
||||
|
||||
$this->assertSame($pipeline('a')('b'), f\compile($pipeline)('a')('b'));
|
||||
}
|
||||
|
||||
public function testCompileUnwrapsClosureOfCompilable()
|
||||
{
|
||||
$spy = new class(strtoupper(...)) extends FunctionPipeline {
|
||||
public int $calls = 0;
|
||||
|
||||
public function __invoke(...$args)
|
||||
{
|
||||
$this->calls++;
|
||||
return parent::__invoke(...$args);
|
||||
}
|
||||
};
|
||||
|
||||
$this->assertSame('A', f\compile(\Closure::fromCallable($spy))('a'));
|
||||
$this->assertSame('A', f\compile($spy(...))('a'));
|
||||
$this->assertSame(0, $spy->calls);
|
||||
}
|
||||
|
||||
public function testCompiledPredicatesMatchPredicates()
|
||||
{
|
||||
$values = [0, 1, 2, 3, '1', 'x'];
|
||||
$cases = [
|
||||
[f\predicate(fn ($x) => $x), $values],
|
||||
[f\predicate(is_numeric(...)), $values],
|
||||
[p\gt(1), $values],
|
||||
[p\gt(1)->not(), $values],
|
||||
[f\not(fn ($x) => $x > 1), $values],
|
||||
[f\all(p\gt(0), p\lt(3)), $values],
|
||||
[f\any(p\lt(1), p\gt(2)), $values],
|
||||
[f\always(), $values],
|
||||
[f\never(), $values],
|
||||
[p\property('x', p\gt(1)), [(object) ['x' => 1], (object) ['x' => 2]]],
|
||||
];
|
||||
|
||||
foreach ($cases as [$predicate, $inputs]) {
|
||||
$compiled = f\compile($predicate);
|
||||
|
||||
foreach ($inputs as $value) {
|
||||
$this->assertSame($predicate($value), $compiled($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCompiledPredicateDoesNotInvokePredicate()
|
||||
{
|
||||
$inner = new class(fn ($x) => $x > 1) extends ClosurePredicate {
|
||||
public int $calls = 0;
|
||||
|
||||
public function __invoke(...$args): bool
|
||||
{
|
||||
$this->calls++;
|
||||
return parent::__invoke(...$args);
|
||||
}
|
||||
};
|
||||
|
||||
$compiled = f\compile(f\all($inner->not(), p\lt(5)));
|
||||
|
||||
$this->assertTrue($compiled(1));
|
||||
$this->assertFalse($compiled(2));
|
||||
$this->assertSame(0, $inner->calls);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user