13

I'm having an array_reduce function which I am willing to exit when specific criteria is met.

$result = array_reduce($input, function($carrier, $item) {
  // do the $carrier stuff
  if (/* god was one of us */) {
    break; //some break analogue
  }
  return $carrier;
});

How do I achieve this? Or should I use foreach instead?

Konstantin Bodnia
  • 1,372
  • 3
  • 20
  • 43

5 Answers5

5

array_reduce is used to write functional-style code which always iterates over the full array. You can either rewrite to use a regular foreach loop to implement short circuiting logic, or you can simply return the current $carrier unmodified. This will still iterate over your full array, but it will not alter the result (as you said, this is more alike to continue)

knittl
  • 246,190
  • 53
  • 318
  • 364
2

According to a similar answer.

Break array_walk from anonymous function

The best and the worst way to complete this is Exception. Not recommend this way, but this way is the solution to your question:

try {
    $result = array_reduce( $input, function ( $carrier, $item ) {
        // do the $carrier stuff
        $condition = true;
        if ( $condition ) {
            throw new Exception;
        }

        return $carrier;
    } );
} catch ( Exception $exception ) {
    echo 'Break';
}

The way I would solve the problem

I would create a global function or write PHP extension and add a function

There is a good answer about writing PHP extension: How to make a PHP extension

array_reduce_2();

But there is a problem with breaking implementation. Need to detect which condition to out of function. Below implementation, array_reduce_2 checks if a callback returned. If so - breaking out of execution.

This way allows checking if execution has broken by checking return type of value.

array_reduce_2 implementation

According to @wordragon notice, implemented the ability to pass an associative array as param too.

function array_reduce_2( $array, $callback, $initial = null ) {
    $len   = count( $array );
    $index = 0;

    if ( $len == 0 && count( func_get_args() ) == 1 ) {
        return null;
    }

    $values = array_values( $array );
    $result = $initial ?? $values[ $index ++ ];
    foreach ( $values as $value ) {
        $result = $callback( $result, $value );

        if ( ! is_callable( $result ) ) {
            continue;
        }

        // break;
        return $result;
    }

    return $result;
}

I've used the idea from JS implementation and rewrite for PHP accordingly https://gist.github.com/keeto/229931

Detecting if the break occured

$input  = [ 'test', 'array', 'god was one of us' ];
$result = array_reduce_2( $input, function ( $carrier, $item ) {
    // do the $carrier stuff
    if ( $item === 'god was one of us' ) {
        return function () {
            return 'god was one of us';
        };
    }

    return $carrier;
} );

$is_break = is_callable( $result );
if ( $is_break ) {
    echo $result();
    exit;
}

Important to note!

This array_reduce_2 implementation works properly only if you don't need to return the normal value as a callback.

123
  • 2,169
  • 3
  • 11
  • 35
  • 1
    This also assumes the array is numerically keyed, which array_reduce does not require. You could easily fix this by converting the array to a numerically indexed array with array_values. – wordragon Mar 15 '21 at 03:46
2

Firstly, let me say that array_reduce is probably one of my favorite functions - I am famous (well, in a very small circle) for taking 40 lines of clearly written code and replacing them with four harder-to-follow 10 line array_reduce calls to do the same thing!

Sadly, PHP array functions seem bound to want to complete their task. This, combined with the inability to make a recursive unnamed function, makes this common situation difficult to deal with. Not wanting to put a lot of ugly for loops in my code, I tend to bury them in another function (see reduce below) as did an earlier poster.

It's worth pointing out that this is in no way as efficient as using array functions, and, in most circumstances, it's better just to let the array reduce function use a "done" flag to spin quickly through the unneeded values. At any rate, this is something reasonably array_reduce like (the evaluation function using a null return to indicate its finished). The goal is to add up the numbers in the array until you get to a 4.

<?php

$init = 0;
$arr = [1,2,3,4,5,6,7,8,9,0];

$func = function($c, $it) {
            if ($it == 4) return null;
            return $c + $it;
        };

function reduce($arr, $f, $init) {
    for ($c = $init; count($arr); ) {
        $newc = $f($c, array_shift($arr));
        if (!isset($newc)) break;
        $c = $newc;
    }
    return $c;
}

echo reduce($arr, $func, $init) . "\n";   // 6
wordragon
  • 1,297
  • 9
  • 16
1

I suggest using foreach loops instead. The reasons to not use array_reduce are:

Sound reasons:

  1. It is not statically type-checked. So code inspections do not show type errors if there are any in the input or callback arguments.
  2. It returns mixed, so inspections do not show errors if you misuse the result, or they may show false positive if you use it properly.
  3. You cannot break.

Opinionated reasons:

  1. It is harder on the eye. Having a $result and adding to it in a loop (or whatever you do) is way easier to read than grasping that something is returned and then passed as a $carry accumulator in the next call.
  2. It makes me lazy to extract functions properly. If I extract one operation to a callback, I then may find the code short enough to not extract the whole array operation to a function which should really be done in the first place.
  3. If you use a condition to break, there is a good chance you may one day need other arguments to that callback function. With the callback signature being fixed, you would have to pass arguments with use keyword which really much harder to read than a non-callback.
Alexey Inkin
  • 1,853
  • 1
  • 12
  • 32
0

breakable_array_reduce()

function breakable_array_reduce(array $array, callable $callback, $initial = null) {
    $result = $initial;
    foreach ($array as $value) {
        $ret = $callback($result, $value);
        if (false === $ret) {
            return $result;
        } else {
            $result = $ret;
        }
    }
    return $result;
}

Usage

// array of 10 values
$arr = [
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1
];

// callback function which stops once value >= 5
$sum_until_five = function($initial, $value) {
    if ($initial >= 5) {
        return false;
    } else {
        return $initial + $value;
    }
};

// calculate sum of $arr using $sum_until_five()
$sum = breakable_array_reduce($arr, $sum_until_five);

// output: 5
echo $sum;

Explanation

breakable_array_reduce() will work just like array_reduce() unless/until callback $callback returns bool(false)

Alternate implementation using array keys:

breakable_array_reduce_keyed()

function breakable_array_reduce_keyed(array $array, callable $callback, $initial = null) {
    $result = $initial;
    foreach ($array as $key => $value) {
        $ret = $callback($result, $value, $key);
        if (false === $ret) {
            return $result;
        } else {
            $result = $ret;
        }
    }
    return $result;
}

Usage

// array of values
$arr = [
    'foo' => 1,
    'bar' => 1,
    'baz' => 1
];

// callback function which stops when $key === 'baz'
$sum_until_baz = function($initial, $value, $key) {
    if ('baz' === $key) {
        return false;
    } else {
        return $initial + $value;
    }
};

// calculate sum of $arr using $sum_until_baz()
$sum = breakable_array_reduce($arr, $sum_until_baz);

// output: 2
echo $sum;

P.S. I just wrote and fully tested this locally.

caffeinatedbits
  • 471
  • 5
  • 8