I have a method which takes a generator plus some additional parameters and returns a new generator:
function merge(\Generator $carry, array $additional)
{
foreach ( $carry as $item ) {
yield $item;
}
foreach ( $additional as $item ) {
yield $item;
}
}
The usual use case for this function is similar to this:
function source()
{
for ( $i = 0; $i < 3; $i++ ) {
yield $i;
}
}
foreach ( merge(source(), [4, 5]) as $item ) {
var_dump($item);
}
But the problem is that sometimes I need to pass empty source to the merge method. Ideally I would like to be able to do something like this:
merge(\Generator::getEmpty(), [4, 5]);
Which is exactly how I would do in C# (there is a IEnumerable<T>.Empty property). But I don't see any kind of empty generator in the manual.
I've managed to work around this (for now) by using this function:
function sourceEmpty()
{
if ( false ) {
yield;
}
}
And this works. The code:
foreach ( merge(sourceEmpty(), [4, 5]) as $item ) {
var_dump($item);
}
correctly outputs:
int(4)
int(5)
But this is obviously not an ideal solution. What would be the proper way of passing an empty generator to the merge method?
Bit late, but needed an empty generator myself, and realized creating one is actually quite easy...
function empty_generator(): Generator
{
yield from [];
}
Don't know if that's better than using the EmptyIterator, but this way you get exactly the same type as non-empty generators at least.
Just for completeness, perhaps the least verbose answer so far:
function generator() {
return; yield;
}
I just wondered about the same question and remembered an early description in the docs (which should be in at least semantically until today) that a generator function is any function with the yield keyword.
Now when the function returns before it yields, the generator should be empty.
And so it is.
Example on 3v4l.org: https://3v4l.org/iqaIY
I've found the solution:
Since \Generator extends \Iterator I can just change the method signature to this:
function merge(\Iterator $carry, array $additional)
{
// ...
This is input covariance thus it would break backward compatibility, but only if someone did extend the merge method. Any invocations will still work.
Now I can invoke the method with PHP's native EmptyIterator:
merge(new \EmptyIterator, [4, 5]);
And the usual generator also works:
merge(source(), [4, 5])
As explained in the official docs, you can create an in-line Generator instance, by using yield in an expression:
$empty = (yield);
That should work, but when I tried using that, I got a fatal error (yield expression can only be used in a function). Using null didn't help either:
$empty = (yield null); //error
So I guess you're stuck with the sourceEmpty function... it was the only thing I found that works... note that it will create a null value in the array you're iterating.
All the code was tested on PHP 5.5.9, BTW
The best fix I can come up with (seeing as compatibility is an issue) would be to make both arguments optional:
function merge(\Generator $carry = null, array $additional = array())
{
if ($carry)
foreach ($carry as $item)
yield $item;
foreach ($additional as $item)
yield $item;
}
foreach(merge(null, [1,2]) as $item)
var_dump($item);
This way, existing code won't brake, and instead of constructing an empty generator, passing null will work just fine, too.
Related
I have this function in my old php5 code that will accept a variable number of parameters and perform sorting based on the parameters:
function array_alternate_multisort(){
$arguments = func_get_args();
$arrays = $arguments[0];
for ($c = (count($arguments)-1); $c > 0; $c--)
{
if (in_array($arguments[$c], array(SORT_ASC , SORT_DESC)))
{
continue;
}
$compare = create_function('$a,$b','return strcasecmp($a["'.$arguments[$c].'"], $b["'.$arguments[$c].'"]);');
usort($arrays, $compare);
if ($arguments[$c+1] == SORT_DESC)
{
$arrays = array_reverse($arrays);
}
}
return $arrays ;
}
I call it like this:
$alliances = array_alternate_multisort($alliances, "output", SORT_DESC, "score", SORT_DESC);
How can I replace this with a function without calling create_function()?
You can use an anonymous function instead:
$compare = function ($a, $b) use ($arguments, $c) {
return strcasecmp($a[$arguments[$c]], $b[$arguments[$c]]);
};
Untested but should be close enough
The use keyword allows you to inherit variables from the parent scope inside your function.
First of all, I'll argue that if you have the ability to hardcode this into your script:
$alliances = array_alternate_multisort($alliances, "output", SORT_DESC, "score", SORT_DESC);
then you can just as easily completely scrap your custom function and just write this:
Code: (Demo)
array_multisort(...[
array_column($alliances, 'output'),
SORT_DESC,
array_column($alliances, 'score'),
SORT_DESC,
&$alliances
]);
This will do EVERYTHING that your custom function will do and more WITHOUT introducing ANY custom function.
This is a very concise, totally native, and instantly readable technique. Using this means that:
you will not confine your script to only SORT_ASC and SORT_DESC; there are other useful sorting flags to enjoy for specific scenarios.
you can opt to omit the sorting direction parameters if you wish to use SORT_ASC (the default sorting flag).
you modify the input array by reference like other native sorting functions.
Now anything beyond the above is going to introduce unnecessary convolution. To keep this hypothetical (which, again, I don't endorse) demonstration simple I'll insist that the sorting direction flags are required like in your original snippet.
Code: (Demo)
function array_alternate_multisort($array, ...$args) {
foreach ($args as $i => $arg) {
$sortParams[] = $i & 1 ? $arg : array_column($array, $arg);
}
$sortParams[] = &$array;
array_multisort(...$sortParams);
return $array;
}
& 1 is a bitwise odd check. If the index is odd, push the constant into $sortParams otherwise push the column data into $sortParams.
This answer belongs to a family of similar answers that use the splat operator to unpack parameters into a array_multisort() call.
array_multisort and dynamic variable options
Sort array of associative arrays on multiple columns using specified sorting rules
According to the documentation it if possible to delegate generation from any Traversable object. However I see the difference between yield from {a Generator instance} and yield from {an Iterator instance}.
An example.
This piece of code has an Iterator and a Generator that are doing the same: they provide the "1, 2, 3" sequence:
$iterator = new class implements \Iterator {
private $values = [1, 2, 3];
public function current()
{
return current($this->values);
}
public function next()
{
next($this->values);
}
public function key()
{
return key(this->values);
}
public function valid()
{
return current($this->values) !== false;
}
public function rewind()
{
reset($this->values);
}
};
$generator = (function ()
{
yield 1;
yield 2;
yield 3;
})();
But when I try to yield from them, I've got different results. Let's play with this function:
function skipOne(\Iterator $iterator)
{
$iterator->next();
yield from $iterator;
}
With a generator everything works as expected:
foreach (skipOne($generator) as $value) {
var_dump($value);
}
Output:
int(2)
int(3)
But with an Iterator it doesn't skip the first number:
foreach (skipOne($iterator) as $value) {
var_dump($value);
}
Output:
int(1)
int(2)
int(3)
I've found that yield from operator cause $iterator->rewind() invocation by some reason. What I'm doing wrong?
You are not doing anything wrong.
yield from will attempt to rewind something that's capable of being rewound, but generators are not, they only go forward.
That`s the reason your code works as you expect in the first example, and not in the second.
You could wrap your $iterator in a generator, and you'd get the same result.
$wrappedIterator = (function($iterator) {
foreach ($iterator as $value) {
yield $value;
}
})($iterator);
You are simply takind "advantage" of the one-use nature of the generators, since they can only go forward and never be rewound.
They same would happen with foreach, for example, since it also attempts to rewind whatever is looping about.
If you simply did
$iterator->next();
foreach ($iterator as $value) {
var_dump($value);
}
you would also get:
int(1)
int(2)
int(3)
In both cases (yield from and foreach) the iterator will be rewound.
Although if you tried
$generator->next();
foreach ($generator as $value) {
var_dump($value);
}
you would get a fatal error.
In your case, since when yield from is not capable of rewinding the object of its affections it doesn't complain and proceeds as if nothing happened, it can be confusing.
You can easily verify the behavior by checking this.
As others have stated, yield from will call $iterator->rewind().
Generators cannot be rewound, because their rewind() is implemented to do nothing.
From the PHP Manual:
Generators are forward-only iterators, and cannot be rewound once iteration has started. This also means that the same generator can't be iterated over multiple times: the generator will need to be rebuilt by calling the generator function again.
So if you wish to have the same behavior as Generators, you have two options.
Option 1: No-op Rewind
Simply leave the rewind() method of your Iterator empty, and move any rewind code to the constructor of the iterator.
class MyIterator implements \Iterator
{
public function __construct()
{
// put rewind code here
}
public function rewind()
{
// Do nothing
}
}
This will make your iterators less reusable, while also making it unclear from outside code why the iterator is not behaving like a regular iterator. So use this option with caution.
Options 2: Use NoRewindIterator
NoRewindIterator is an SPL iterator class that decorates another iterator.
$noRewindIterator = new \NoRewindIterator($iterator);
It will behave identically to the $iterator it is given, with the exception of the rewind() method. When $noRewindIterator->rewind() is called, nothing will happen.
This has the advantage of allowing you to write normal rewind-able iterators, while also being able to do partial iteration.
Here's how your skipOne() function can use NoRewindIterator:
function skipOne(\Iterator $iterator)
{
$iterator->next();
yield from new \NoRewindIterator($iterator);
}
PHP documentation said The outer generator will then yield all values from the inner generator, object, or array until that is no longer valid, after which execution will continue in the outer generator.
All is one from keywords here. PHP source code is proof that too.
I'm quite happy that PHP 7.1 introduced the iterable pseudo-type.
Now while this is great when just looping over a parameter of this type, it is unclear to me what to do when you need to pass it to PHP functions that accept just an array or just a Traversable. For instance, if you want to do an array_diff, and your iterable is a Traversable, you will get an array. Conversely, if you call a function that takes an Iterator, you will get an error if the iterable is an array.
Is there something like iterable_to_array (NOT: iterator_to_array) and iterable_to_traversable?
I'm looking for a solution that avoids conditionals in my functions just to take care of this difference, and that does not depend on me defining my own global functions.
Using PHP 7.1
Not sure this is what are you searching for but this is the shortest way to do it.
$array = [];
array_push ($array, ...$iterable);
I'm not very sure why it works. Just I found your question interesting and I start fiddling with PHP
Full example:
<?php
function some_array(): iterable {
return [1, 2, 3];
}
function some_generator(): iterable {
yield 1;
yield 2;
yield 3;
}
function foo(iterable $iterable) {
$array = [];
array_push ($array, ...$iterable);
var_dump($array);
}
foo(some_array());
foo(some_generator());
It would be nice if works with function array(), but because it is a language construct is a bit special. It also doesn't preserve keys in assoc arrays.
For php >= 7.4 this works pretty well out of the box:
$array = [...$iterable];
See https://3v4l.org/L3JNH
Edit: Works only as long the iterable doesn't contain string keys
Can be done like this:
$array = $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
Is there something like iterable_to_array and iterable_to_traversable
Just add these to your project somewhere, they don't take up a lot of space and give you the exact APIs you asked for.
function iterable_to_array(iterable $it): array {
if (is_array($it)) return $it;
$ret = [];
array_push($ret, ...$it);
return $ret;
}
function iterable_to_traversable(iterable $it): Traversable {
yield from $it;
}
Terms are easy to mix
Traversable
Iterator (I see this as a concrete type, like user-defined class A)
IteratorAggregate
iterable (this is a pseudo-type, array or traversable are accepted)
array (This is a concrete type, and it's not exchangeable with Iterator in context of that a Iterator type is required)
arrayIterator (can be used to convert array to iterator)
So, that's why if function A(iterable $a){}, then it accepts parameter of either array or an instanceof traversable (Iterator, IteratorAggregate are both accepted because it's obvious these two classes implement Traversable. In my test, passing ArrayIterator also works ).
In case Iterator type is specified for parameter, passing in an array will cause TypeError.
You can use iterator_to_array converting your variable to Traversable first:
$array = iterator_to_array((function() use ($iterable) {yield from $iterable;})());
Conversion method is taken from the comment under this question.
Here is working demo.
For the "iterable to array" case it seems there is no single function call you can make and that you'll either need to use a conditional in your code or define your own function like this one:
function iterable_to_array( iterable $iterable ): array {
if ( is_array( $iterable ) ) {
return $iterable;
}
return iterator_to_array( $iterable );
}
For the "iterable to Iterator" case things are much more complicated. Arrays can be easily translated into a Traversable using ArrayIterator. Iterator instances can just be returned as they are. That leaves Traversable instances that are not Iterator. On first glance it looks like you can use IteratorIterator, which takes a Traversable. However that class is bugged and does not work properly when giving it an IteratorAggregate that returns a Generator.
The solution to this problem is too long to post here though I have created a mini-library that contains both conversion functions:
function iterable_to_iterator( iterable $iterable ): Iterator
function iterable_to_array( iterable $iterable ): array
See https://github.com/wmde/iterable-functions
Starting with PHP 8.2 the iterator_to_array() and iterator_count() functions will accept iterable instead of Traversable. Thus they will start to accept arrays and do what you would expect them to do when encountering an array.
Specifically the following equalities hold:
iterator_to_array($array, true) == $array
iterator_to_array($array, false) == array_values($array)
and
iterator_count($array) == count($array)
More details can be found in the corresponding RFC: PHP RFC: Make the iterator_*() family accept all iterables.
I need to write a simple script that loads data from multiple files and merges it somehow. However, given the fact that the files might be quite huge I'd like to load data partially. To do so I decided to use yield. And according to examples I found I could use following construction for single generator:
$generator = $someClass->load(); //load method uses yield so it returns generator object
foreach($generator as $i) {
// do something
}
But what if I want to use two generators at once?
$generatorA = $someClass1->load(); //load method uses yield so it returns generator object
$generatorB = $someClass2->load(); //load method uses yield so it returns generator object
foreach($generatorA as $i) {
// how can I access to resultSet from generatorB here?
}
Generators in PHP implement the Iterator interface, so you can merge / combine multiple Generators like you can combine multiple Iterators.
If you want to iterate over both generators one after the other (merge A + B), then you can make use of the AppendIterator.
$aAndB = new AppendIterator();
$aAndB->append($generatorA);
$aAndB->append($generatorB);
foreach ($aAndB as $i) {
...
If you want to iterate over both generator at once, you can make use of MultipleIterator.
$both = new MultipleIterator();
$both->attachIterator($generatorA);
$both->attachIterator($generatorB);
foreach ($both as list($valueA, $valueB)) {
...
Example for those two incl. examples and caveats are in this blog-post of mine as well:
Iterating over Multiple Iterators at Once
Otherwise I don't understand what you've been asking for. If you could clarify, I should be able to give you more directions.
From https://www.php.net/manual/en/language.generators.syntax.php#control-structures.yield.from
Generator delegation via yield from
In PHP 7, generator delegation allows you to yield values from another
generator, Traversable object, or array by using the yield from keyword.
The outer generator will then yield all values from
the inner generator, object, or array until that is no longer valid,
after which execution will continue in the outer generator.
So it's possible to combine two (or more) generators using yield from.
/**
* Yield all values from $generator1, then all values from $generator2
* Keys are preserved
*/
function combine_sequentially(Generator $generator1, Generator $generator2): Generator
{
yield from $generator1;
yield from $generator2;
};
Or something more fancy (here, it's not possible to use yield from):
/**
* Yield a value from $generator1, then a value from $generator2, and so on
* Keys are preserved
*/
function combine_alternatively(Generator $generator1, Generator $generator2): Generator
{
while ($generator1->valid() || $generator2->valid()) {
if ($generator1->valid()) {
yield $generator1->key() => $generator1->current();
$generator1->next();
}
if ($generator2->valid()) {
yield $generator2->key() => $generator2->current();
$generator2->next();
}
}
};
While AppendIterator works for Iterators, it has some issues.
Firstly it is not so nice to need to construct a new object rather than just calling a function. What is even less nice is that you need to mutate the AppendIterator, since you cannot provide the inner iterators in its constructor.
Secondly AppendIterator only takes Iterator instances, so if you have a Traversable, such as IteratorAggregate, you are out of luck. Same story for other iterable that are not Iterator, such as array.
This PHP 7.1+ function combines two iterable:
/**
* array_merge clone for iterables using lazy evaluation
*
* As with array_merge, numeric elements with keys are assigned a fresh key,
* starting with key 0. Unlike array_merge, elements with duplicate non-numeric
* keys are kept in the Generator. Beware that when converting the Generator
* to an array with a function such as iterator_to_array, these duplicates will
* be dropped, resulting in identical behaviour as array_merge.
*
*
* #param iterable ...$iterables
* #return Generator
*/
function iterable_merge( iterable ...$iterables ): Generator {
$numericIndex = 0;
foreach ( $iterables as $iterable ) {
foreach ( $iterable as $key => $value ) {
yield is_int( $key ) ? $numericIndex++ : $key => $value;
}
}
}
Usage example:
foreach ( iterable_merge( $iterator1, $iterator2, $someArray ) as $k => $v ) {}
This function is part of a small library for working with iterable, where it is also extensively tested.
If you want to use Generators with AppendIterator you'll need to use NoRewindIterator with it:
https://3v4l.org/pgiXB
<?php
function foo() {
foreach ([] as $foo) {
yield $foo;
}
}
$append = new AppendIterator();
$append->append(new NoRewindIterator(foo()));
var_dump(iterator_to_array($append));
Trying to traverse a bare Generator with AppendIterator will cause a fatal error if the Generator never actually calls yield:
https://3v4l.org/B4Qnh
<?php
function foo() {
foreach ([] as $foo) {
yield $foo;
}
}
$append = new AppendIterator();
$append->append(foo());
var_dump(iterator_to_array($append));
Output:
Fatal error: Uncaught Exception: Cannot traverse an already closed generator in /in/B4Qnh:10
Stack trace:
#0 [internal function]: AppendIterator->rewind()
#1 /in/B4Qnh(10): iterator_to_array(Object(AppendIterator))
#2 {main}
thrown in /in/B4Qnh on line 10
Process exited with code 255.
You can use yield from
function one()
{
yield 1;
yield 2;
}
function two()
{
yield 3;
yield 4;
}
function merge()
{
yield from one();
yield from two();
}
foreach(merge() as $i)
{
echo $i;
}
An example Reusable function
function iterable_merge( iterable ...$iterables ): Generator {
foreach ( $iterables as $iterable ) {
yield from $iterable;
}
}
$merge=iterable_merge(one(),two());
Something like:
$generatorA = $someClass1->load(); //load method uses yield so it returns generator object
$generatorB = $someClass2->load(); //load method uses yield so it returns generator object
$flag = true;
$i = 0;
while($flag === false) {
if ($i >= count($generatorA) || $i >= count($generatorB)) {
$flag = true;
}
// Access both generators
$genA = $generatorA[$i];
$genB = $generatorB[$i];
$i++;
}
Try this:
<?php
foreach($generatorA as $key=>$i) {
$A=$i;//value from $generatorA
$B=$generatorB[$key];//value from $generatorB
}
while reading abt array_filter() from php manual,came to face example to demostrate
the same function using callback function as given below
<?php
function odd($var)
{
// returns whether the input integer is odd
return($var & 1);
}
function even($var)
{
// returns whether the input integer is even
return(!($var & 1));
}
$array1 = array("a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5);
$array2 = array(6, 7, 8, 9, 10, 11, 12);
echo "Odd :\n";
print_r(array_filter($array1, "odd"));
echo "Even:\n";
print_r(array_filter($array2, "even"));
?>
can you please help me to know how actually calback function calling,actual parameter pass,working?
any link to demostrate about callback wld be great help.
In the two given examples, array_filter will go over the values in the passed array and send each value in it to the callback function (odd or even). The callback function will then inspect the value to see whether it is odd or even and return TRUE or FALSE. If it returns FALSE, the value is filtered from the array.
The easiest way to find out what your function is passing to your callback is to supply a callback that prints the passed arguments, e.g.
array_filter($anArray, function() { var_dump(func_get_args()) });
Callbacks are described in detail at
http://de.php.net/manual/en/language.pseudo-types.php#language.types.callback
Imagine you have a function like this:
function getNumbersDivisibleBy3($arr)
{
$threes = array();
foreach($arr as $val)
{
if($val % 3 == 0)
{
$threes[] = $val;
}
}
return $threes
}
This function filters out all the numbers divisible by three from an array and returns them as another array.
Now imagine another function:
function GetWordsStartingWithC($arr)
{
$cs = array();
foreach($arr as $word)
{
if($word[0] == 'C')
{
$cs[] = $word;
}
}
return $cs;
}
This function filters out all the words that start with C from an array of words and returns them another array.
If you look at the above functions, their meta function (as it were) can be explained as "This functions filters out all the items in an array that satisfies a condition and returns them as another array."
So instead of having to write the same boiler plate code to iterate through a list and filter out all elements that match, the PHP developers have written a function that takes an array and a string that is a function name.
In other languages, such as C#, instead of a string that is a function name, you actually pass in an object called a delegate, which is a pointer or reference to a function. But in PHP they have ways of figuring out which function you mean by the name you pass in. I don't know what those are.
So the array_filter function could look something like this (it won't as it's probably not written in PHP)
function array_filter($arr, $callbackname)
{
$result = array();
foreach($arr as $item)
{
if(call_user_func($callbackname, $item))
{
$result[] = $item;
}
}
return $result;
}
In that function you can see how similar it is to the previous two, but instead of a predefined condition, it calls back (using the call_user_func() function) the function to be used via the name you passed in and applies it to each item in the array by using each item as a parameter for the function.
So you can reduce the amount of code you write by using array_filter because you don't have to write the boiler plate iteration code, just the conditional function you need to filter on.
A callback function is a function that it "called back" by another one. In the case of array_filter() the callback is invoked for every element of the array, and that element is the argument passed to it. You don't control what argument are passed, that's up the the main function you're using.
So you basically say to array_filter(): please run through this array and apply this function named "odd" to every element; this function will return a boolean value so you know what to keep and what to discard.