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;
|
||||
|
||||
use Kadet\Functional\Decorator;
|
||||
use function Kadet\Functional\closure;
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
/**
|
||||
* Compiles a collection callback, adapted to its arity.
|
||||
*
|
||||
* 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
|
||||
* never mistaken for an optional argument.
|
||||
* `trim(...)`, also when wrapped in decorators) and non-variadic closures declaring exactly one parameter receive
|
||||
* only the value, so the key is never mistaken for an optional argument.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@ -24,10 +26,38 @@ function callback(callable $callable): \Closure
|
||||
{
|
||||
$compiled = compile($callable);
|
||||
$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 $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
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@ -43,6 +43,21 @@ class CallbackTest extends \PHPUnit\Framework\TestCase
|
||||
$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()
|
||||
{
|
||||
$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'));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
$partial = f\partial(str_replace(...), 'a', _, 'banana');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user