Download stream.js
2Kb minified
What are streams?
Streams are a data structure that is simple to operate, much like an array or a linked list, but with some additional extraordinary capabilities.
What’s so special about them?
Unlike arrays, streams are a magical data structure. It can hold an infinite number of elements. Yes, you heard it right. His magic comes from his ability to lazily execute. This simple term makes perfect sense that they can load an infinite number of elements.
Getting Started
If you are willing to spend 10 minutes reading this article, your understanding of programming may be completely changed (unless you have experience in functional programming!). Please be patient and let me first introduce the basic functional operations supported by streams that are very similar to arrays or linked lists. Then I'll introduce you to some of its very interesting features.
Stream is a container. It holds the elements. You can use Stream.make to make a stream load some elements. Just pass the desired elements as parameters:
var s = Stream.make( 10, 20, 30 ); // s is now a stream containing 10, 20, and 30
is enough Simple, now s is a stream with 3 elements: 10, 20, and 30; in order. We can use s.length() to check the length of this stream, and use s.item( i ) to retrieve an element inside by index. You can also get the first element of this stream by calling s.head(). Let’s do it in action:
var s = Stream.make ( 10, 20, 30 );
console.log( s.length() ); // outputs 3
console.log( s.head() ); // outputs 10
console.log ( s.item( 0 ) ); // exactly equivalent to the line above
console.log( s.item( 1 ) ); // outputs 20
console.log( s.item( 2 ) ) ; // outputs 30
This page has loaded the stream.js class library. If you want to run these examples or write a few sentences of your own, just open your browser's Javascript console and run them directly.
Let’s continue, we can also use new Stream() or directly use Stream.make() to construct an empty stream. You can use the s.tail() method to get all the remaining elements in the stream except the first element. If you call the s.head() or s.tail() method on an empty stream, an exception will be thrown. You can check if a stream is empty using s.empty(), which returns true or false.
var s = Stream.make( 10, 20, 30 );
var t = s.tail(); // returns the stream that contains two items: 20 and 30
console.log( t.head() ); // outputs 20
var u = t.tail(); // returns the stream that contains one item: 30
console.log( u.head() ); // outputs 30
var v = u.tail(); // returns the empty stream
console.log( v.empty() ); // prints true
Doing this will print out all the elements in a stream:
var s = Stream.make( 10, 20, 30 );
while ( !s.empty() ) {
console.log( s.head() );
s = s.tail();
}
We have a simple Method to achieve this: s.print() will print out all elements in the stream.
What else can you use them for?
Another convenient function is the Stream.range( min, max ) function. It returns a stream containing natural numbers from min to max.
var s = Stream.range( 10, 20 );
s.print(); // prints the numbers from 10 to 20
On this stream, you can use functions such as map, filter, and walk. s.map( f ) accepts a parameter f, which is a function. All elements in the stream will be processed by f; its return value is the stream processed by this function. So, for example, you can use it to double the number in your stream:
function doubleNumber( x ) {
return 2 * x;
}
var numbers = Stream.range( 10, 15 );
numbers.print(); // prints 10, 11, 12, 13, 14, 15
var doubles = numbers.map( doubleNumber );
doubles.print(); // prints 20, 22, 24, 26, 28, 30
Cool, isn’t it? Similarly, s.filter( f ) also accepts a parameter f, which is a function. All elements in the stream will be processed by this function; its return value is also a stream, but only contains elements that allow the f function to return true. . So, you can use it to filter to specific elements in your stream. Let's use this method to build a new stream containing only odd numbers based on the previous stream:
function checkIfOdd( x ) {
if ( x % 2 == 0 ) {
// even number
return false;
}
else {
// odd number
return true;
}
}
var numbers = Stream.range( 10, 15 );
numbers.print(); // prints 10, 11, 12, 13, 14, 15
var onlyOdds = numbers.filter( checkIfOdd );
onlyOdds.print(); // prints 11, 13, 15
very effective , isn’t it? The last s.walk(f) method also accepts a parameter f, which is a function. All elements in the stream must be processed by this function, but it will not have any impact on the stream. Our idea of printing all elements in the stream has a new implementation method:
function printItem( x ) {
console.log( 'The element is: ' x );
}
var numbers = Stream.range( 10, 12 );
// prints :
// The element is: 10
// The element is: 11
// The element is: 12
numbers.walk( printItem );
There is also a very useful function: s.take(n), which returns a stream containing only the first n elements in the original stream. This is useful when used to intercept streams:
var numbers = Stream.range( 10, 100 ); // numbers 10...100
var fewerNumbers = numbers.take( 10 ); // numbers 10...19
fewerNumbers.print();
Some other useful things: s.scale( factor ) will multiply all elements in stream by factor (factor); s.add( t ) will make each element of stream s and stream t The corresponding elements in are added, and the result of the addition is returned. Let’s look at a few examples:
var numbers = Stream .range( 1, 3 );
var multiplesOfTen = numbers.scale( 10 );
multiplesOfTen.print(); // prints 10, 20, 30
numbers.add( multiplesOfTen ).print( ); // prints 11, 22, 33
Although what we have seen so far are operations on numbers, the stream can be loaded with anything: strings, Boolean values, functions, objects ; Even other arrays or streams. However, please note that streams cannot load some special values: null and undefined.
Show me your magic!
Now, let’s deal with infinity. You don't need to add infinite elements to the stream. For example, in the Stream.range(low, high) method, you can ignore its second parameter and write Stream.range(low). In this case, there is no upper limit on the data, so the stream is loaded with All natural numbers from low to infinity. You can also ignore the low parameter. The default value of this parameter is 1. In this case, Stream.range() returns all natural numbers.
Does this require infinite amounts of your memory/time/processing power?
No, it won’t. This is the best part. You can run this code and it will run very fast, just like a normal array. Here is an example of printing from 1 to 10:
var naturalNumbers = Stream.range(); // returns the stream containing all natural numbers from 1 and up
var oneToTen = naturalNumbers.take( 10 ); // returns the stream containing the numbers 1...10
oneToTen.print();
You are lying
Yes, I am lying. The key is that you can think of these structures as infinity, which introduces a new programming paradigm, one that strives for simplicity, making your code easier to understand and closer to natural mathematics than the usual imperative programming. paradigm. The Javascript library itself is very short; it is designed according to this programming paradigm. Let's put it to more use; we construct two streams, one for all odd numbers and one for all even numbers.
var naturalNumbers = Stream.range(); // naturalNumbers is now 1, 2, 3, ...
var evenNumbers = naturalNumbers.map( function ( x ) {
return 2 * x;
} ); // evenNumbers is now 2, 4, 6 , ...
var oddNumbers = naturalNumbers.filter( function ( x ) {
return x % 2 != 0;
} ); // oddNumbers is now 1, 3, 5, ...
evenNumbers.take( 3 ).print(); // prints 2, 4, 6
oddNumbers.take( 3 ).print(); // prints 1, 3, 5
Cool, isn’t it? I'm not exaggerating, streams are more powerful than arrays. Now, please bear with me for a few minutes and let me tell you a little more about streams. You can use new Stream() to create an empty stream and new Stream( head, functionReturningTail ) to create a non-empty stream. For this non-empty stream, the first parameter you pass in becomes the head element of the stream, and the second parameter is a function that returns the tail of the stream (a stream containing all the remaining elements), most likely An empty stream. Confused? Let’s look at an example:
var s = new Stream ( 10, function () {
return new Stream();
} );
// the head of the s stream is 10; the tail of the s stream is the empty stream
s .print(); // prints 10
var 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 empty stream.
t.print(); // prints 10, 20, 30
Nothing Looking for trouble? You can do this directly using Stream.make(10, 20, 30). However, please note that this way we can easily construct our infinite stream. Let’s make an endless stream:
function 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
);
}
var s = ones(); // now s contains 1, 1, 1, 1, ...
s.take( 3 ).print(); // prints 1, 1, 1
Please note that if you use s on an infinite stream .print(), it will print endlessly and eventually exhaust your memory. Therefore, you'd better s.take( n ) before using s.print(). Using s.length() on an infinite stream is also pointless, so don't do these operations; it will cause an endless loop (trying to reach the end of an endless stream). But for infinite streams, you can use s.map( f ) and s.filter( f ). However, s.walk(f) is also not useful for infinite streams. All in all, there are some things you have to remember; for infinite streams, be sure to use s.take(n) to take out a finite portion.
Let’s see if we can do something more interesting. There is also an interesting way to create a stream containing natural numbers:
function ones() {
return new Stream( 1, ones );
}
function 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 ).print(); // prints 1, 2, 3 , 4, 5
Careful readers will find out why the second parameter of the newly constructed stream is a function that returns the tail, not the tail itself. This method prevents endless execution cycles by delaying the tail interception operation.
Let’s look at a more complex example. The following is an exercise left for readers. Please indicate what the following code does?
function sieve( s ) {
var h = s.head();
return new Stream( h, function () {
return sieve( s.tail().filter( function( x ) {
return x % h != 0;
} ) );
} );
}
sieve( Stream.range( 2 ) ).take( 10 ).print();
Please be sure Take some time to understand the purpose of this code. Unless you have functional programming experience, most programmers will find this code difficult to understand, so don't be frustrated if you don't see it right away. A little hint for you: find out what is the header element of the stream being printed. Then find out what the second element is (the head element of the remaining elements); then the third element, then the fourth. The name of the function can also give you some hints.
If you are interested in puzzles like this,
here are some more.
If you really can’t figure out what this code does, just run it and see for yourself! This way you can easily understand how it is done.
Tribute
Streams are actually not a new idea. Many functional programming languages support this feature. The so-called 'stream' is the name in the Scheme language, and Scheme is a dialect of the LISP language. The Haskell language also supports infinitely large lists. The names 'take', 'tail', 'head', 'map' and 'filter' all come from the Haskell language. This concept, although different but very similar, also exists in Python and many other Chinese languages. They are all called "generators".
These ideas have been circulating in the functional programming community for a long time. However, it is a very new concept to most Javascript programmers, especially those without functional programming experience.
Many of the examples and ideas here come from the book
Structure and Interpretation of Computer Programs . If you like these ideas, I highly recommend reading it; the book is available online for free. It is also the source of my creativity in developing this Javascript library.
If you like streams in other syntax forms, you can try linq.js, or if you use node.js, node-lazy may be more suitable for you.
If you like CoffeeScript, Michael Blume is porting stream.js to CoffeeScript to create coffeestream.
Thank you for reading!
I hope you find something useful and enjoy stream.js. The library is free, so if you like it, or it helps in some way, you might consider buying me a hot chocolate drink (I don't drink coffee) or drop me a line. If you plan to do this, please indicate where you are from and what you do. I love collecting images from all over the world, so please include photos from your city!