15

Is there a way of doing array_map but as an iterator?

For example:

foreach (new MapIterator($array, $function) as $value)
{
   if ($value == $required)
      break;
}

The reason to do this is that $function is hard to calculate and $array has too many elements, only need to map until I find a specific value. array_map will calculate all values before I can search for the one I want.

I could implement the iterator myself, but I want to know if there is a native way of doing this. I couldn't find anything searching PHP documentation.

Ezequiel
  • 668
  • 2
  • 9
  • 25

8 Answers8

6

This is a lazy collection map function that gives you back an Iterator:

/**
 * @param array|Iterator $collection
 * @param callable $function
 * @return Iterator
 */
function collection_map( $collection, callable $function ) {
    foreach( $collection as $element ) {
        yield $function( $element );
    }
}
Jeroen De Dauw
  • 10,321
  • 15
  • 56
  • 79
6

In short: No.

There is no lazy iterator mapping built into PHP. There is a non-lazy function iterator_apply(), but nothing like what you are after.

You could write one yourself, as you said. I suggest you extend IteratorIterator and simply override the current() method.

If there were such a thing it would either be documented here or here.

ezzatron
  • 779
  • 6
  • 15
  • It's indeed a good idea to use `IteratorIterator` rather than writing a complete decorator implementing the `Iterator` interface. – Harmen Apr 09 '17 at 21:12
1

I'm thinking of a simple Map class implementation which uses an array of keys and an array of values. The overall implementation could be used like Java's Iterator class whereas you'd iterate through it like:

while ($map->hasNext()) {
  $value = $map->next();
  ...
}
jerluc
  • 4,186
  • 2
  • 25
  • 44
0

Don't bother with an iterator, is the answer:

foreach ($array as $origValue)
{
   $value = $function($origValue);
   if ($value == $required)
      break;
}
Izkata
  • 8,961
  • 2
  • 40
  • 50
  • Aaaand this was downvoted because? No one else provided an example of _how_ to do what the OP asked, without side-effects. – Izkata Apr 18 '12 at 17:54
  • I didn't downvote it, but that's not a lazy solution, just a short-circuiting solution. It only covers the example, not the whole range of situations that the OP likely intends. (Although the accepted answer doesn't provide any code, it does point in the right direction.) – Brilliand Oct 12 '12 at 15:31
0

I wrote this class to use a callback for that purpose. Usage:

$array = new ArrayIterator(array(1,2,3,4,5));
$doubles = new ModifyIterator($array, function($x) { return $x * 2; });

Definition (feel free to modify for your need):

class ModifyIterator implements Iterator {
    /**
     * @var Iterator
     */
    protected $iterator;

    /**
     * @var callable Modifies the current item in iterator
     */
    protected $callable;

    /**
     * @param $iterator Iterator|array
     * @param $callable callable This can have two parameters
     * @throws Exception
     */
    public function __construct($iterator, $callable) {
        if (is_array($iterator)) {
            $this->iterator = new ArrayIterator($iterator);
        }
        elseif (!($iterator instanceof Iterator))
        {
            throw new Exception("iterator must be instance of Iterator");
        }
        else
        {
            $this->iterator = $iterator;
        }

        if (!is_callable($callable)) {
            throw new Exception("callable must be a closure");
        }

        if ($callable instanceof Closure) {
            // make sure there's one argument
            $reflection = new ReflectionObject($callable);
            if ($reflection->hasMethod('__invoke')) {
                $method = $reflection->getMethod('__invoke');
                if ($method->getNumberOfParameters() !== 1) {
                    throw new Exception("callable must have only one parameter");
                }
            }
        }

        $this->callable = $callable;
    }

    /**
     * Alters the current item with $this->callable and returns a new item.
     * Be careful with your types as we can't do static type checking here!
     * @return mixed
     */
    public function current()
    {
        $callable = $this->callable;
        return $callable($this->iterator->current());
    }

    public function next()
    {
        $this->iterator->next();
    }

    public function key()
    {
        return $this->iterator->key();
    }

    public function valid()
    {
        return $this->iterator->valid();
    }

    public function rewind()
    {
        $this->iterator->rewind();
    }
}
noisecapella
  • 814
  • 7
  • 17
0

PHP's iterators are quite cumbersome to use, especially if deep nesting is required. LINQ, which implements SQL-like queries for arrays and objects, is better suited for this, because it allows easy method chaining and is lazy through and through. One of the libraries implementing it is YaLinqo*. With it, you can perform mapping and filtering like this:

// $array can be an array or \Traversible. If it's an iterator, it is traversed lazily.
$is_value_in_array = from($array)->contains(2);

// where is like array_filter, but lazy. It'll be called only until the value is found.
$is_value_in_filtered_array = from($array)->where($slow_filter_function)->contains(2);

// select is like array_map, but lazy.
$is_value_in_mapped_array = from($array)->select($slow_map_function)->contains(2);

// first function returns the first value which satisfies a condition.
$first_matching_value = from($array)->first($slow_filter_function);
// equivalent code
$first_matching_value = from($array)->where($slow_filter_function)->first();

There're many more functions, over 70 overall.

* developed by me

Athari
  • 33,702
  • 16
  • 105
  • 146
0

Have a look at Non-standard PHP library. It has a lazy map function:

use function \nspl\a\lazy\map;

$heavyComputation = function($value) { /* ... */ };
$iterator = map($heavyComputation, $list);
Ihor Burlachenko
  • 4,689
  • 1
  • 26
  • 25
0
foreach ($array as $key => $value) {
   if ($value === $required) {
      break;
   } else {
      $array[$key] = call_back_function($value);
   }
}

process and iterate until required value is found.

dqhendricks
  • 19,030
  • 11
  • 50
  • 83