pass value only to decorated internal callbacks
This commit is contained in:
parent
66705f3e31
commit
159e9a56ab
@ -9,14 +9,16 @@
|
|||||||
|
|
||||||
namespace Kadet\Functional\Collections\Internal;
|
namespace Kadet\Functional\Collections\Internal;
|
||||||
|
|
||||||
|
use Kadet\Functional\Decorator;
|
||||||
|
use function Kadet\Functional\closure;
|
||||||
use function Kadet\Functional\compile;
|
use function Kadet\Functional\compile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiles a collection callback, adapted to its arity.
|
* Compiles a collection callback, adapted to its arity.
|
||||||
*
|
*
|
||||||
* The returned closure is called with ($value, $key). Internal functions (including first-class callables like
|
* The returned closure is called with ($value, $key). Internal functions (including first-class callables like
|
||||||
* `trim(...)`) and non-variadic closures declaring exactly one parameter receive only the value, so the key is
|
* `trim(...)`, also when wrapped in decorators) and non-variadic closures declaring exactly one parameter receive
|
||||||
* never mistaken for an optional argument.
|
* only the value, so the key is never mistaken for an optional argument.
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -24,10 +26,38 @@ function callback(callable $callable): \Closure
|
|||||||
{
|
{
|
||||||
$compiled = compile($callable);
|
$compiled = compile($callable);
|
||||||
$reflection = new \ReflectionFunction($compiled);
|
$reflection = new \ReflectionFunction($compiled);
|
||||||
|
$target = new \ReflectionFunction(closure(target($callable)));
|
||||||
|
|
||||||
if ($reflection->isInternal() || (!$reflection->isVariadic() && $reflection->getNumberOfParameters() === 1)) {
|
if ($target->isInternal() || (!$reflection->isVariadic() && $reflection->getNumberOfParameters() === 1)) {
|
||||||
return static fn ($value) => $compiled($value);
|
return static fn ($value) => $compiled($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $compiled;
|
return $compiled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the function a callable ultimately calls, by unwrapping decorators, also when they were turned into closures.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
function target(callable $callable): callable
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
if ($callable instanceof Decorator) {
|
||||||
|
$callable = $callable->getDecorated();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($callable instanceof \Closure) {
|
||||||
|
$reflection = new \ReflectionFunction($callable);
|
||||||
|
$bound = $reflection->getClosureThis();
|
||||||
|
|
||||||
|
if ($bound instanceof Decorator && $reflection->getName() === '__invoke') {
|
||||||
|
$callable = $bound;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $callable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,10 @@ namespace Kadet\Functional;
|
|||||||
interface Compilable
|
interface Compilable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Returns a closure that behaves exactly like invoking this object, but does not go through it on each call.
|
* 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.
|
||||||
*/
|
*/
|
||||||
public function compile(): \Closure;
|
public function compile(): \Closure;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,21 @@ class CallbackTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSame('a', callback(f\partial(trim(...), _, 'x'))('xax', 'key'));
|
$this->assertSame('a', callback(f\partial(trim(...), _, 'x'))('xax', 'key'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPartialOfInternalFunctionDoesNotFillOptionalParameterWithKey()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a', 'b'], callback(f\partial(explode(...), ','))('a,b', 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartialOfInternalFunctionWithoutArgumentsGetsValueOnly()
|
||||||
|
{
|
||||||
|
$this->assertSame('a', callback(f\partial(trim(...)))(' a ', 'k'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompiledMemoizedInternalFunctionGetsValueOnly()
|
||||||
|
{
|
||||||
|
$this->assertSame('a', callback(f\compile(f\memoize(trim(...))))(' a ', 'k'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testDecoratorIsCompiled()
|
public function testDecoratorIsCompiled()
|
||||||
{
|
{
|
||||||
$callback = callback(f\partial(fn ($prefix, $v, $k) => "$prefix$k=$v", '>'));
|
$callback = callback(f\partial(fn ($prefix, $v, $k) => "$prefix$k=$v", '>'));
|
||||||
|
|||||||
@ -103,6 +103,22 @@ class CompileTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSame('aa', f\compile(f\curry(str_repeat(...), null, 'a'))(2, 'extra'));
|
$this->assertSame('aa', f\compile(f\curry(str_repeat(...), null, 'a'))(2, 'extra'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCompileWithFewerArgumentsThanPlaceholdersMatchesDecorator()
|
||||||
|
{
|
||||||
|
$partial = f\partial(str_repeat(...), _, _);
|
||||||
|
|
||||||
|
$this->assertSame($this->outcome(fn () => $partial('a')), $this->outcome(fn () => f\compile($partial)('a')));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function outcome(callable $call)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return ['result', $call()];
|
||||||
|
} catch (\Throwable $error) {
|
||||||
|
return ['error', get_class($error), $error->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testCompileFunctionDelegatesToMethod()
|
public function testCompileFunctionDelegatesToMethod()
|
||||||
{
|
{
|
||||||
$partial = f\partial(str_replace(...), 'a', _, 'banana');
|
$partial = f\partial(str_replace(...), 'a', _, 'banana');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user