2
\$\begingroup\$

I'm trying to determine if an instance of an object exists within a collection.

I'm using array_walk_recursive to walk and check the leaves of an array.

I've added a small short circuit, but still all leaves will be iterated over.

I could alternatively use something like an exception which is thrown on the first find, to break out early, but that feels kind of wrong.

Am I missing a simple alternative?

<?php

class Fish {}
class Mammal {}
class Whale extends Mammal {}
class Human extends Mammal {}
class Shark extends Fish {}

function anyFish(array $collection)
{
    $found = false;
    array_walk_recursive($collection, function ($v) use (&$found) {
        if(!$found && $v instanceof Fish)
            $found = true;
    });

    return $found;
}

$seas = [
    'pacific'  => [new Whale, new Shark],
    'atlantic' => [new Human, new Shark]
];

$pub = [new Human, new Human];

var_dump(anyFish($seas));
var_dump(anyFish($pub));

Output:

bool(true)
bool(false)

Found on Stackoverflow: https://stackoverflow.com/questions/17853113/break-array-walk-from-anonymous-function)

\$\endgroup\$
  • \$\begingroup\$ Just want to make a note for anyone investigating alternatives. php.net/manual/en/class.recursivearrayiterator.php#106519 \$\endgroup\$ – mickmackusa Jan 26 at 23:22
  • \$\begingroup\$ Wouldn't you rather have logic like: !$found && so that you are not overwriting true with true? \$\endgroup\$ – mickmackusa Jan 26 at 23:26
  • \$\begingroup\$ @mickmackusa, of course that's exactly what I meant! Logic error on my part there, thanks. \$\endgroup\$ – Progrock Jan 27 at 18:57
  • \$\begingroup\$ Here is an example of using an exception to break out of the lambda on the first find of an instance: 3v4l.org/OvMMm this avoids leafing through everything. But feels dirty. \$\endgroup\$ – Progrock Jan 27 at 19:04
  • \$\begingroup\$ Yes, I saw that in my researching. I don't disagree about the dirtiness. \$\endgroup\$ – mickmackusa Jan 27 at 20:32
2
\$\begingroup\$

I think good coding practices should prioritize efficiency before code brevity. It is important to not ask for unnecessary work to be done (this is part of the artistry of being a clever coder). Implementing an early exit, in my opinion is a non-negotiable factor in selecting the "best" code design.

Using a function like array_walk_recursive() is handy for its leafnode traversal, but I agree that the syntax of making an early exit is a little unsightly. For this reason, I recommend a language construct as part of the recursive design. The conditional logic is the only part that can be condensed, so I've tried to boil it down as much as possible.

Code: (Demo)

function anyFish(array $collection)
{
    foreach ($collection as $item)
    {
        if((is_array($item) && anyFish($item)) || $item instanceof Fish)
        {
            return true;
        }
    }
    return false;
}

I don't think it is horrible to look at, but everybody loves their own babies. I'm not sure that I can make it any more readable.

\$\endgroup\$
  • \$\begingroup\$ I see what you are saying, but I am hesitant to ditch the curly braces as a matter of personal preference. If you are looking to do some "code golf" go ahead, but I am in this camp: stackoverflow.com/a/8726411/2943403 . \$\endgroup\$ – mickmackusa Jan 27 at 22:41
  • 1
    \$\begingroup\$ @Progrock this is not Python, PHP is C-like language. And it is not a matter of taste, there is a standard: php-fig.org/psr/psr-2 \$\endgroup\$ – Your Common Sense Jan 28 at 5:00
0
\$\begingroup\$

Here is an example of using recursion, it stops soon after an instance is found, so doesn't iterate through the entire collection.

<?php
class Fish {}
class Mammal {}
class Whale extends Mammal {}
class Human extends Mammal {}
class Shark extends Fish {}

function anyFish($collection)
{
    $found = false;
    rFish($collection, $found);

    return $found;
}
function rFish(array $collection, &$found)
{   
    if($found)
        return;
    foreach($collection as $item)
    {
        if(is_array($item))
        {
            rFish($item, $found);
        }
        elseif($item instanceof Fish)
        {
            $found = true;
            break;
        }
    }
}
$seas = [
    'pacific'  => [new Whale, new Whale, new Shark, new Whale],
    'atlantic' => [new Human, new Shark]
];

$pub = [new Human, new Human];

var_dump(anyFish($seas));
var_dump(anyFish($pub));

Output:

bool(true)
bool(false)

@mickmackusa distilled the recursive function beautifully.

I'm adding my generalised solution here:

function hasInstance(array $collection, $class)
{
    foreach($collection as $item)
        if(is_array($item) && hasInstance($item, $class) || $item instanceof $class)
            return true;

    return false;
}

A call would be like:

hasInstance($seas, Fish::class);
\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.