DEV Community

david duymelinck
david duymelinck

Posted on • Edited on

Php fun: Currying with some spice

For people that are not familiar with currying, it is a way to write functions that use a function call per argument instead of a list of arguments.

This is useful when the first or last arguments are semi constant.
For example you need to remove the first character of a string many times. Instead of using substr($string, 1) you could use $removeFirstChar($string), that had 1 and null curried before using it.

A helper function or language construct can be used to make an existing function with a argument list into a curried function. function(1, 2, 3) becomes function(1)(2)(3).

Plain curry

function curry(callable $fn, ...$args) {
    $reflection = new ReflectionFunction($fn);
    $numArgs = $reflection->getNumberOfParameters();

    if (count($args) >= $numArgs) {
        return $fn(...$args);
    }
    // until the total arguments of the curried function are not added
    // return an anonymus function containing the previous function call data
    return function (...$nextArgs) use ($fn, $args, $numArgs) {
        $allArgs = array_merge($args, $nextArgs);
        if (count($allArgs) >= $numArgs) {
            return $fn(...$allArgs);
        }
        return curry($fn, ...$allArgs);
    };
}

// the volunteer function
function adder(int $a, int $b, int $c, int $d) 
{
   return $a + $b + $c + $d;    
}

// currying in action
$first = curry('adder', 1); 
$secondAndThird = $first(2)(3); // you can also write $first(2, 3)
echo $secondAndThird(4);
Enter fullscreen mode Exit fullscreen mode

This curry function can only be used to curry the first arguments.

A problem I see with the curry function is that the type hinting is only checked when the last argument call is made.
It would be better to have an exception on the line the bad value is added.

Curry with garlic

function curry(callable $fn, ...$args) {
    $reflection = new ReflectionFunction($fn);
    $numArgs = $reflection->getNumberOfParameters();

    if (count($args) >= $numArgs) {
        return $fn(...$args);
    }
    // no getParameter method so we need to get them all.
    $refArgs = $reflection->getParameters();

    foreach($args as $i => $arg) {
        // mapping with the reflection type names.
        $argTypeName = match(gettype($arg)) {
            'integer' => 'int',
            'boolean' => 'bool',
            default => gettype($arg)
        };
        $refArgType = $refArgs[$i]->getType();
        // do nothing if the argument has no type
        if(is_null($refArgType)) {
            continue;
        }
        // gather one or more types based on the reflection class.
        $refArgTypeNames = $refArgType instanceof ReflectionNamedType ?
          [$refArgType->getName()] :
          array_map(fn($t) => $t->getName(), $refArgType->getTypes())
        ;

        if(! in_array($argTypeName, $refArgTypeNames)) {
            throw new InvalidArgumentException("Argument value $i of curried $fn function has the wrong type.");
        }
    }

    return function (...$nextArgs) use ($fn, $args, $numArgs) {
        $allArgs = array_merge($args, $nextArgs);
        if (count($allArgs) >= $numArgs) {
            return $fn(...$allArgs);
        }
        return curry($fn, ...$allArgs);
    };
}

// the volunteer function
function adder(int $a, int|string $b, $c, $d) 
{
   return $a + $b + $c + $d;    
}

// currying in action
$first = curry('adder', '1'); // returns the exception 
Enter fullscreen mode Exit fullscreen mode

The next feature is to be aware of optional arguments.

Curry with cayenne pepper

// The variable length argument of the curry function makes this the only
// possibility to add settings without changing the function signature. 
enum CurrySetting
{
   case CountDefaults;
}

function curry(callable $fn, ...$args) {
    $reflection = new ReflectionFunction($fn);
    $refArgs = $reflection->getParameters();
    $numArgs = in_array(CurrySetting::CountDefaults, $args) ? $reflection->getNumberOfRequiredParameters() : $reflection->getNumberOfParameters() ;
    $executableArgs = array_filter($args,  fn($v) =>  $v !== CurrySetting::CountDefaults);

    if (count($executableArgs) >= $numArgs) {
        return $fn(...$executableArgs);
    }

    foreach($executableArgs as $i => $arg) {
        $argTypeName = match(gettype($arg)) {
            'integer' => 'int',
            'boolean' => 'bool',
            default => gettype($arg)
        };
        $refArgType = $refArgs[$i]->getType();

        if(is_null($refArgType)) {
            continue;
        }

        $refArgTypeNames = [];

        if($refArgType instanceof ReflectionNamedType) {
            $refArgTypeNames[] = $refArgType->getName();
        } else {
            $refArgTypeNames = array_map(fn($t) => $t->getName(), $refArgType->getTypes());
        }

        if(! in_array($argTypeName, $refArgTypeNames)) {
            throw new InvalidArgumentException("Argument value $i of curried $fn function has the wrong type.");
        }
    }

    return function (...$nextArgs) use ($fn, $args, $numArgs) {
        $allArgs = array_merge($args, $nextArgs);
        $executableArgs = array_filter($args,  fn($v) =>  $v !== CurrySetting::CountDefaults);

        if (count($executableArgs) >= $numArgs) {
            return $fn(...$executableArgs);
        }
        return curry($fn, ...$allArgs);
    };
}

// volunteer function
function adder(int $a, int|string $b, $c, $d = 2) 
{
   return $a + $b + $c + $d;    
}

// currying
$first = curry('adder', 1);
$secondAndThird = $first(2)(3);
echo $secondAndThird(4);

$f  = curry('adder', 1, CurrySetting::CountDefaults);
echo $f(2, 3);
Enter fullscreen mode Exit fullscreen mode

With the CurrySetting enum in place, it is easy to add currying from the last argument to the first. I'm going to leave that as an exercise.

The dish is served

I could add the possibility to let the curry arguments define the position instead of using the function call order. But there is a little voice in my head that warns me about the many edge cases.
This would be curry with very spicy chilies.

Currying is a more complex programming technique, but the more you know the easier it is to find solutions.

Top comments (0)