0

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?

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Armin66
  • 39
  • 1
  • 5
  • I don't think PHP has a good function for this. You could write your own `array_find_first()` function that takes an array and a callback function, like `array_filter()`, and returns the first index where the callback function returns true. – Barmar Aug 23 '22 at 20:06
  • I have no clue how to use array_filter() in my function. I am searching for the index the first element in array, where parameter $days is <= then value of this element. – Armin66 Aug 23 '22 at 20:14
  • You wouldn't use array_filter. I was just using that as an example of a function that takes an array and callback. You would model your function on that. – Barmar Aug 23 '22 at 20:15
  • 1
    Why are you afraid of loops? They are a core construct of every single programming language, and reimplementing your current solution as a recursive function would just be a more complicated way to write the same loop. – Sammitch Aug 23 '22 at 20:27
  • Rel: [Get the highest key in array less than 'x'](https://stackoverflow.com/q/60006361/2943403) , [Find number which is greater than or equal to N in an array](https://stackoverflow.com/q/6147356/2943403), [How can I get the min value in an array above a certain number?](https://stackoverflow.com/q/7878925/2943403), [Compare one number with numbers in array to find the smallest difference](https://stackoverflow.com/q/56979893/2943403); Distantly related: [Find the position of the first occurring of any number bigger than certain number in a string](https://stackoverflow.com/q/54769824/2943403) – mickmackusa Aug 23 '22 at 22:33
  • _"but I know, that I should use recursive call instead"_ - why? Says who? What advantage could that possibly have over a simple loop here? Especially considering that there is nothing "recursive" about your data to begin with. – CBroe Aug 24 '22 at 07:46

4 Answers4

3

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);
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • this is much better than IłyaBursov's solution as it stop iteration as soon as the first match is found. `array_filter` will always exhaust the entire array. – Mulan Aug 23 '22 at 22:36
0

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);
}
Iłya Bursov
  • 23,342
  • 4
  • 33
  • 57
  • Great, it works fine. Only one problem: my original function in this situation: ``` $index =indexByDateRange(array(30,60,90,120,360), 475); ``` returned 5 , so it indicated , that 475 is beyond defined range (array has last index 4 so 5 means beyond the array). In your version of this function it returns null. I prefere the function will return 5 instead of null. Any idea how to do it? – Armin66 Aug 23 '22 at 20:30
  • @Armin66 I've updated function to cover situation when input is out of the range – Iłya Bursov Aug 23 '22 at 20:36
  • Thanks. It works now the same way as original version. – Armin66 Aug 23 '22 at 20:46
0

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
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
0

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.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Shouldn't the returned index for 500 be `4`? Is the ternary after `array_walk()` [(which only returns true)](https://www.php.net/manual/en/function.array-walk.php#:~:text=Return%20Values%20%C2%B6-,Returns%20true.,-Errors/Exceptions%20%C2%B6) just to play nice with the arrow function syntax? This feels like a lot of extra effort to conditionally break a function that is not designed to break, no? This would not be the sort of code that I'd enjoy maintaining in a project. – mickmackusa Aug 24 '22 at 06:04
  • @mickmackusa there's two sets of test data in his post. I used the second set which has ranges `[30,60,90,120,360]`, so index for 500 should be `5`. The idea isn't to design something to break `array_walk`. `return` is a keyword and a side effect. The goal is to show `callcc` offers means to a _functional_ form of return, and has the flexibility of any ordinary function. This subtle difference allows `callcc` to be composed with other expressions, enabling more functional style in your programs. – Mulan Aug 24 '22 at 17:01
  • But there is no index 5 in an indexed array with 5 elements. – mickmackusa Aug 25 '22 at 05:07
  • 1
    @mickmackusa i see what happened now. you edited the post to fix the indexes in the ranges, but really the OP forgot to include the 91-120 range at index 3. The original introduction of the data and the sample code all include the 120. With the original question in tact, it’s obvious `5` should be returned for the infinite range – Mulan Aug 25 '22 at 14:03