stream.php
stream.php is a PHP class that unlocks a new data structure for you to use: streams.
require_once 'stream/stream.php';Download stream.php
What is a stream?
A stream is an easy-to-use data structure similar to a linked list, but with some extraordinary capabilities.
What's so special about them?
Unlike linked lists, streams can act as though they have an infinite number of elements. Yes, that is correct. Their power is derived from lazy evaluation: only those items that are absolutely required for the current expression are made available. Streams can represent entire relationships at once, while linked lists and arrays are limited to holding a static set of objects at a given time.
Getting started
Let's start at the beginning and go over the basic operations streams support. After that, we'll discuss about their interesting properties. If you are new to functional programming, give 10 minutes of your time to reading this page. It may completely change the way you think about programming!
Streams contain items. You can make a stream with some items using
Stream::make()
. Just feed it any items you want to be
part of your stream:
$s = Stream::make( 10, 20, 30 ); // s is now a stream containing 10, 20, and 30
Easy enough! $s
is now a stream containing three items: 10, 20, and 30; in this order. We can determine the length of the stream using
$s->length()
and retrieve particular items by index using $s->item( $i )
, or more simply using $s( $i )
.
The first item of the stream can also be obtained by calling $s->head()
. Let's see it in action:
$s = Stream::make( 10, 20, 30 ); echo $s->length(); // outputs 3 echo $s->head(); // outputs 10 echo $s(0); // exactly equivalent to the line above echo $s(1); // outputs 20 echo $s(2); // outputs 30
The blank stream can be constructed either using new Stream()
or just Stream::make()
.
A stream containing all the items of the original except the head can be obtained using $s->tail()
.
Calling $s->head()
or $s->tail()
on a blank stream yields an error.
You can check if a stream is blank using $s->blank()
which returns either true
or false
.
$s = Stream::make( 10, 20, 30 ); $t = $s->tail(); // returns the stream that contains two items: 20 and 30 echo "{$t->head()}\n"; // outputs 20 $u = $t->tail(); // returns the stream that contains one item: 30 echo "{$u->head()}\n"; // outputs 30 $v = $u->tail(); // returns the blank stream echo "{$v->blank()}\n"; // prints true
Here's one way to print all the elements in a stream:
$s = Stream::make( 10, 20, 30 ); while ( !$s->blank() ) { echo "{$s->head()}\n"; $s = $s->tail(); }
There's a convenient shortcut for that: $s->out()
shows all the items in your stream.
Stream PHP also implements the Iterator interface, so you can access elements in the stream using the foreach iterator. Be careful that you aren't iterating forever on an infinite stream!
$s = Stream::make( 10, 20, 30 ); foreach( $s as $value ) { echo "{$value}\n"; }
What else can streams do?
One useful shortcut is the Stream::range( $min, $max )
function. It returns a stream containing the natural numbers from $min
to $max
.
$s = Stream::range( 10, 20 ); $s->out(); // prints the numbers from 10 to 20
You can also use map
, filter
, and walk
on your streams. $s->map( $f )
takes as an argument a function $f
and evaluates $f( $i )
on
every element $i
of the stream. It returns a stream of the return values of that function. So you can use it to, for example, double the numbers in your stream:
$doubleNumber = function ( $x ) { return 2 * $x; }; $numbers = Stream::range( 10, 15 ); $numbers->out(); // prints 10, 11, 12, 13, 14, 15 $doubles = $numbers->map( $doubleNumber ); $doubles->out(); // prints 20, 22, 24, 26, 28, 30
Cool, right? Similarly $s->filter( $f )
takes as an argument a function $f
and evaluates $f( $i )
on every element $i
of the stream; it then returns
a stream containing only those elements $i
for which $f( $i )
returned true
. So you can use it as a filter to keep only those elements in your stream that satisfy a certain condition.
Let's use this function to filter out all of the even numbers from a stream:
$checkIfOdd = function ( $x ) { return $x % 2 != 0; }; $numbers = Stream::range( 10, 15 ); $numbers->out(); // prints 10, 11, 12, 13, 14, 15 $onlyOdds = $numbers->filter( $checkIfOdd ); $onlyOdds->out(); // prints 11, 13, 15
Useful, right?! Finally $s->walk( $f )
takes as an argument a function $f
and evaluates $f( $i )
for all elements $i
of the stream. It doesn't affect
the stream in any way. For example, here's another way to print all of a stream's elements:
$printItem = function ( $x ) { echo "The element is: {$x}\n"; }; $numbers = Stream::range( 10, 12 ); // prints: // The element is: 10 // The element is: 11 // The element is: 12 $numbers->walk( $printItem );
One more useful function: $s->take( $n )
returns a stream with the first $n
elements of your original stream. That's useful
for slicing streams:
$numbers = Stream::range( 10, 100 ); // numbers 10...100 $fewerNumbers = $numbers->take( 10 ); // numbers 10...19 $fewerNumbers->out();
A few more: $s->scale( $k )
multiplies every element $i
of your stream by some constant factor $k
; and $s->add( $t )
adds each
element of the stream $s
to each element of the stream $t
and returns the result. Let's see an example of this:
$numbers = Stream::range( 1, 3 ); $multiplesOfTen = $numbers->scale( 10 ); $multiplesOfTen->out(); // prints 10, 20, 30 $numbers->add( $multiplesOfTen )->out(); // prints 11, 22, 33
Although we've only seen streams of numbers until now, you can also have streams of anything: strings, booleans, functions, objects; even arrays or other streams. A stream's elements need not all be of the same type, either.
Be aware, however, that your streams may not contain the value null
as an item.
Show me the magic!
Now let's start playing with infinity. Streams need not have a finite number of elements. For example, omitting the second argument toStream::range( $low, $high )
creates a stream containing natural numbers greater than or equal to $low
. Omitting $low
defaults to 1
, causing Stream::range()
to return the stream of all natural numbers.
Does that require infinite memory/time/processing power?
No, it doesn't. That's the awesome part! You can run these things and they work really fast, like regular lists. Here's an example that prints the numbers from 1 to 10:
$naturalNumbers = Stream::range(); // returns the stream containing all natural numbers from 1 and up $oneToTen = $naturalNumbers->take( 10 ); // returns the stream containing the numbers 1...10 $oneToTen->out();
This is cheating
Absolutely this is cheating. The point is that you can think of these structures as infinite, and this introduces a new programming paradigm that yields concise code that is easy to understand and closer to mathematics than usual imperative programming. If you prefer, think about streams the same way you think about functions: a function represents an entire relationship at one time. You can find out about how some inputs are related to an output only observing that output. The same is true of a stream; we needn't know every result at the outset. We only need to grab values as they're demanded.
More hot, streamy goodness
Let's play with this a little more and construct streams containing all even numbers and all odd numbers respectively.
$naturalNumbers = Stream::range(); // naturalNumbers is now 1, 2, 3, ... $evenNumbers = $naturalNumbers->map( function ( $x ) { return ( 2 * $x ); }); // evenNumbers is now 2, 4, 6, ... $oddNumbers = $naturalNumbers->filter( function ( $x ) { return ( $x % 2 != 0 ); }); // oddNumbers is now 1, 3, 5, ... $evenNumbers->take( 3 )->out(); // prints 2, 4, 6 $oddNumbers->take( 3 )->out(); // prints 1, 3, 5
Cool, right? I kept my promise that streams are more powerful than lists. Now, let's introduce a few more things about streams. You can create new stream objects using new Stream()
to create a blank stream, or
new Stream( $head, $functionReturningTail )
to create a non-blank stream. In case of a non-blank stream, the first parameter is the head of your
desired stream, while the second parameter is a function returning the tail (a stream with all the rest of the elements), which could potentially be the
blank stream. Confusing? Let's look at an example:
$s = new Stream( 10, function () { return new Stream(); }); // the head of the s stream is 10; the tail of the s stream is the blank stream $s->out(); // prints 10 $t = new Stream( 10, function () { return new Stream( 20, function () { return new Stream( 30, function () { return new Stream(); }); }); }); // the head of the t stream is 10; its tail has a head which is 20 and a tail which // has a head which is 30 and a tail which is the blank stream. $t->out(); // prints 10, 20, 30
Too much trouble for nothing? You can always use Stream::make( 10, 20, 30 )
to do this.
But notice that this way we can construct our own infinite streams easily. Let's make a stream which is an endless series of ones:
$ones = function () use ( &$ones ) { return new Stream( // the first element of the stream of ones is 1... 1, // and the rest of the elements of this stream are given by calling the function ones() (this same function!) $ones ); } $s = $ones(); // now s contains 1, 1, 1, 1, ... $s->take( 3 )->out(); // prints 1, 1, 1
Notice that if you use $s->out()
on an infinite stream, it will print forever, eventually running out of memory. Therefore it's best
to $s->take( $n )
before you $s->out()
. Using $s->length()
on infinite streams is meaningless, so don't do it;
it will cause an infinite loop (trying to find the end of an endless stream). But of course you can use $s->map( $f )
and $s->filter( $f )
on infinite streams. However, $s->walk( $f )
will also not run properly on infinite streams. So those are some things to keep in mind;
make sure you use $s->take( $n )
if you want to take a finite part of an infinite stream.
$ones = function() use ( &$ones ) { return new Stream( 1, $ones ); } $naturalNumbers = function() use ( $ones, &$naturalNumbers ) { return new Stream( // the natural numbers are the stream whose first element is 1... 1, function () { // and the rest are the natural numbers all incremented by one // which is obtained by adding the stream of natural numbers... // 1, 2, 3, 4, 5, ... // to the infinite stream of ones... // 1, 1, 1, 1, 1, ... // yielding... // 2, 3, 4, 5, 6, ... // which indeed are the REST of the natural numbers after one return $ones()->add( $naturalNumbers() ); } ); } $naturalNumbers()->take( 5 )->out(); // prints 1, 2, 3, 4, 5
Astute readers will by now have realized why the second parameter to new Stream is a function that returns the tail and not the tail itself. This way we can avoid infinite loops by postponing when the tail is evaluated. Lazy evaluation is an amazing thing.
Let's try a slightly harder example. It's left as an exercise for the reader to figure out what the following piece of code does.
function sieve( $s ) { $h = $s->head(); return new Stream( $h, function () use ( $s, $h ) { return sieve( $s->tail()->filter( function( $x ) use ( $h ) { return ( $x % $h != 0 ); })); }); } sieve( Stream::range( 2 ) )->take( 10 )->out();
Do take some time to figure out what this does. Most find it hard to understand unless they have a functional programming background, so don't feel bad if you don't get it immediately. Here's a hint: what is the head of the printed stream? The second element of the stream (the head of the tail)? The third element? The name of the function may also help you.
If you really can't figure out what it does, just run it and see for yourself! It'll be easier to figure out how it does it then.
Tribute
Streams aren't a new idea at all. Many functional languages support them. The name 'stream' is used in Scheme, a LISP dialect that supports these features. Haskell also supports infinite lists. The names 'take', 'tail', 'head', 'map' and 'filter' are all used in Haskell. A different but similar concept also exists in Python and in many other languages; these are called "generators".
These ideas have been around for a long time in the functional programming community. However, they're quite new concepts for most PHP and Javascript programmers, especially those without a functional programming background.
Many of the examples and ideas come from the book Structure and Interpretation of Computer Programs. If you like the ideas here, it comes highly recommended; it's available online for free. It was the original inspiration for building this library.
Dionysis Zindros is the original author of stream.js: streamjs.org