Avoid loop in PHP function and adhere to principles of functional programming

266 Views Asked by At

I would like to solve the following problem while following the principles of functional programming. I have an array with "date To" ranges with these values: [30,60,90,120,360] and this array represents days intervals:

Index : Interval
- - - - - - - - - 
0 : 0-30 
1 : 31-60
2 : 61-90
3 : 91–360
4 : 361 – infinity

Now I have a value of x , let's say 75 days. Using functional programming, how do I find out (according to the days intervals defined in the array) that this value belongs to an interval with index 2?

I solved this algorithm in function with loop, but I know, that I should use recursive call instead. I don't know how.

Here is my code:

function indexByDateRange(array $intervalRange, int $days) {
    foreach ($intervalRange as $i=>$value) {
        if ($days <= $value) {
            return $i;
        }
    }
    return count($intervalRange);
}

$index = indexByDateRange(array(30,60,90,120,360), 73); // 73 is between 61 and 90 , so should return index = 2

$index = indexByDateRange(array(30,60,90,120,360), 4; // 4 is smaller then 30 , so should return index = 0

Any suggestion how to rewrite the indexByDateRange function so it will comply with Functional Programming principles?

4

There are 4 best solutions below

1
Barmar On

Define a function that takes an array and a callback function. This will use a loop like the one that you wrote to find the first array element that satisfies the function. Then you can call that in your functional programming style.

function array_search_callback($callback, $array) {
    foreach ($array as $i => $el) {
        if (($callback)($el)) {
            return $i;
        }
    }
    return count($array);
}

$intervalRange = [30,60,90,120,360];
$days = 73;
$index = array_search_callback(function($value) use ($days) {
    return $days <= $value;
}, $intervalRange);
3
Iłya Bursov On

just quick sample how you can do it in more functional way:

function indexByDateRange(array $intervalRange, int $days) {
    return array_key_first(array_filter(
        $intervalRange,
        function($v) use ($days) {
            return $days <= $v;
        })) ?? count($intervalRange);
}
0
mickmackusa On

I don't see any compelling reason to use recursion for such a basic, linear value searching function.

Simply find the key of the element which is the first to meet or exceed the target value. A conditional break or return will ensure best performance.

Code: (Demo)

function greatestIndexWhereValueLessThan($haystack, $needle) {
    foreach ($haystack as $key => $value) {
        if ($value >= $needle) {
            break;
        }
    }
    return $key ?? null;
}

echo greatestIndexWhereValueLessThan([30, 60, 90, 120, 360], 73); // 2
echo "\n";
echo greatestIndexWhereValueLessThan([30, 60, 90, 120, 360], 4);  // 0
4
Mulan On

return is not a function, it's a keyword

array_walk is the functional form of foreach (...) { ... }. You need a way to stop it early as soon as the first match is founds, but return simply won't work here. Enter callcc. callcc calls its user-supplied function with the current contintuation, providing it an $exit function which is essentially the functional form of the return keyword.

function indexByDateRange(array $ranges, int $days) {
  return callcc(fn($exit) =>
    array_walk($ranges, fn($v, $k) => 
      $days <= $v && $exit($k)
    )
    ? count($ranges)
    : null
  );
}

As soon as $exit is called, array_walk is halted and no additional iteration happens -

$ranges = [30,60,90,120,360];

echo "index for (73): ", indexByDateRange($ranges, 73), PHP_EOL;
// 2

echo "index for (4): ", indexByDateRange($ranges, 4), PHP_EOL;
// 0

echo "index for (500): ", indexByDateRange($ranges, 500), PHP_EOL;
// 5

callcc can be implemented as an ordinary function -

class Box extends Error {
  public function __construct($value) {
    $this->unbox = $value;
    throw $this;
  }
}

function callcc(callable $f) {
  try { return $f(fn($value) => new Box($value)); }
  catch (Box $b) { return $b->unbox; }
  catch (Error $e) { throw $e; }
}

Now you are afforded beautiful functional style thanks to arrow expressions, good programmer ergonomics, and early-exit semantics. For more details, see callcc introduced in this Q&A.

array_walk is not functional

PHP has gained better support for functional style in recent versions, but historically PHP wasn't intended to be a programming language. array_walk was added way back in PHP 4 and has an odd behaviour of always returning true.

If you're a PHP programmer trying to write functional programs, you already understand you will need a library of generic functions at your side -

// array_each : ('a array, a' -> void) -> void
function array_each(array $a, callable $f) {
  array_walk($a, $f);
}

When the iterator is exhausted, array_each gives a null return value, allowing the caller to properly use the null coalescing operator, ??. The ternary operator is no longer required -

// indexByDateRange : (int array, int) -> int
function indexByDateRange(array $ranges, int $days) {
  return callcc(fn($exit) =>
    array_each($ranges, fn($v, $k) => 
      $days <= $v && $exit($k)
    )
    ?? count($ranges)
  );
}
// callcc : (('a -> 'b) -> 'a) -> 'a
const callcc = ...

don't get me wrong

Recursion is de facto mechanism for writing "loops" in functional style, but sometimes constructs like foreach are more intuitive. Here we saw how functional style can steal a page from imperative style's book, and still uphold functional principles. Barmar and mickmackusa provide a solution you should use. But maybe you're wondering if indexByDateRange could be a pure functional expression. The above form shows you it's possible and what it looks like.