#!/usr/bin/perl # # Stream.pm # # Sample implementation of lazy, infinite streams with memoization # # Copyright 1997 M-J. Dominus (mjd@pobox.com) # # This program is free software; you can redistribute it and/or modify # it under the terms of any of: # 1. Version 2 of the GNU General Public License as published by # the Free Software Foundation; # 2. Any later version of the GNU public license, or # 3. The Perl `Artistic License' # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the Artistic License with this # Kit, in the file named "Artistic". If not, I'll be glad to provide one. # # You should also have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # package Stream; use Exporter; @ISA = (Exporter); @EXPORT = qw(new iterate tabulate upto iota filter primes merge hamming stats rand list2stream iterate_chop chop_if mingle squares_from hailstones); ### Basic functions ## Manufacture a new stream node with given head and tail. sub new { my $what = shift; my $pack = ref($what) || $what; my ($h, $t) = @_; bless { h => $h, t => $t } => $pack; } ## Return the head of a stream sub head { $_[0]{h}; } ## return the tail of a stream, collecting on a promise ## if necessary sub tail { my $t = $_[0]{t}; if (ref $t eq CODE) { # It is a promise $_[0]{t} = &$t; } $_[0]{t}; } ## Construct an empty stream sub empty { my $pack = ref(shift()) || Stream; bless {e => q{Yes, I'm empty.}} => $pack; } ## Is this stream the empty stream? sub is_empty { exists $_[0]{e}; } ### Tools ## Compute f(n), f(n+1), f(n+2) ... sub tabulate { my $f = shift; my $n = shift; Stream->new(&$f($n), sub { &tabulate($f, $n+1) }); } ## Compute i, f(i), f(f(i)), f(f(f(i))), ... sub iterate { my $f = shift; my $i = shift; Stream->new($i, sub { &iterate($f, &$f($i)) }); } ## Compute list of first n elements of stream. sub take { my $s = shift; my $n = shift; my @r; while ($n-- && !$s->is_empty) { push @r, $s->head; $s = $s->tail; } @r; } ## Return new stream of elements of $s with first ## $n elements skipped. sub drop { my $s = shift; my $n = shift; while ($n-- && !$s->is_empty) { $s = $s->tail; } $s; } ## Actually modify $s to discard first $n elements. ## Return undef if $s was exhausted. sub discard { my $s = shift; my $n = shift; my $d = $s->drop($n); if ($d->is_empty) { $s->{e} = q{Empty.}; delete $s->{h}; delete $s->{t}; } else { $s->{h} = $d->{h}; $s->{t} = $d->{t}; } $s; } ## Display first few elements of a stream $SHOWLENGTH = 10; # Default number of elements to show sub show { my $s = shift; my $len = shift; my $showall = $len eq ALL; $len ||= $SHOWLENGTH; for ($n = 0; $showall || $n < $len; $n++) { if ($s->is_empty) { print "\n"; return; } print $s->head, " "; $s = $s->tail; } print "\n"; } ## $f, $f+1, $f+2, ... $t-1, $t. sub upto { my $f = shift; my $t = shift; return Stream->empty if $f > $t; Stream->new($f, sub { &upto($f+1, $t) }); } ## 1, 2, 3, 4, 5, ... sub iota { &tabulate(sub {$_[0]}, 1); # Tabulate identity function } ## Return a stream of all the elements of s for which predicate p is true. sub filter { my $s = shift; # Second argument is a predicate function that returns true # only when passed an interesting element of $s. my $predicate = shift; # Look for next interesting element until ( $s->is_empty || &$predicate($s->head)) { $s = $s->tail; } # If we ran out of stream, return the empty stream. return $s->empty if $s->is_empty; # Construct new stream with the interesting element at its head # and the rest of the stream, appropriately filtered, # at its tail. Stream->new($s->head, sub { $s->tail->filter($predicate) } ); } ## Given a stream s1, s2, s3, ... return f(s1), f(s2), f(s3), ... sub transform { my $s = shift; return $s->empty if $s->is_empty; my $map_function = shift; Stream->new(&$map_function($s->head), sub { $s->tail->transform($map_function) } ); } # Emit elements of a stream s, chopping it off at the first element # for which `$predicate' is true sub chop_when { my $s = shift; my $predicate = shift; return $s->empty if $s->is_empty || &$predicate($s->head); Stream->new($s->head, sub {$s->tail->chop_when($predicate)}); } # Return first element $h of $s, and sieve out # subsequent elements, discarding those that are divisible by $h. sub prime_filter { my $s = shift; my $h = $s->head; Stream->new($h, sub { $s->tail ->filter(sub { $_[0] % $h }) ->prime_filter() }); } # Multiply every element of a stream $s by a constant $n. sub scale { my $s = shift; my $n = shift; $s->transform(sub { $_[0] * $n }); } # Merge two streams of numbers in ascending order, discarding duplicates sub merge { my $s1 = shift; my $s2 = shift; return $s2 if $s1->is_empty; return $s1 if $s2->is_empty; my $h1 = $s1->head; my $h2 = $s2->head; if ($h1 > $h2) { Stream->new($h2, sub { &merge($s1, $s2->tail) }); } elsif ($h1 < $h2) { Stream->new($h1, sub { &merge($s1->tail, $s2) }); } else { # heads are equal Stream->new($h1, sub { &merge($s1->tail, $s2->tail) }); } } # Given two streams s1, s2, s3, ... and t1, t2, t3, ... # construct s1, t1, s2, t2, s3, t3, ... sub mingle { my $s = shift; my $t = shift; return $t if $s->is_empty; return $s if $t->is_empty; Stream->new($s->head, sub {&mingle($t, $s->tail)}); } # This is not a very good way to do it. sub hamming_slow { my $n = shift; Stream->new($n, sub { &merge(&hamming_slow(2*$n), &merge(&hamming_slow(3*$n), &hamming_slow(5*$n), )) }); } # This is the good one. # # The article says it takes a few minutes to compute 3,000 numbers on # the dinky machine. That turns out to be not because the dinky # machine was slow, but because it had so little memory. With an # extra 24 MB of memory, computing 3,000 numbers takes just under 20 # seconds of CPU time. # sub hamming { my $href = \1; # Dummy reference my $hamming = Stream->new(1, sub { &merge($$href->scale(2), &merge($$href->scale(3), $$href->scale(5) )) } ); $href = \$hamming; # Reference is no longer a dummy $hamming; } # Rujith S. de Silva points out that the `dummy reference' hack # is unneccesary. This version is easier to understand and probably # faster than the `hamming' above: # sub hamming_r { my $hamming; $hamming = Stream->new(1, sub { &merge($hamming_r->scale(2), &merge($hamming_r->scale(3), $hamming_r->scale(5) )) } ); } sub squares_from { my $n = shift; print STDERR "SQUARES_FROM($n)\n" if $DEBUG; Stream->new($n*$n, sub { &squares_from($n+1) }); } # Hailstone number iterator sub next_hail { my $n = shift; ($n % 2 == 0) ? $n/2 : 3*$n + 1; } # Return the Collatz 3n+1 sequence starting from n. sub hailstones { my $n = shift; &iterate(\&next_hail, $n); } # Example random number generator from ANSI C standard sub next_rand { int(($_[0] * 1103515245 + 12345) / 65536) % 32768 } # Stream of random numbers, seeded by $seed. sub rand { my $seed = shift; &iterate(\&next_rand, &next_rand($seed)); } # Auxiliary function for &iterate_chop sub iter_pairs { my $s = shift; my $ss = shift; return $s->empty if $s->is_empty; Stream->new([$s->head, $ss->head], sub {&iter_pairs($s->tail, $ss->tail->tail)} ); } # Given a stream of numbers generated by `iterate', # chop it off before it repeats. # Not guaranteed to do anything useful if applied to a stream that was # not produced by `iterate' sub iterate_chop { my $s = shift; return $s->empty if $s->is_empty; &iter_pairs($s, $s->tail) ->chop_when(sub {$_[0][0] == $_[0][1]}) ->transform(sub {$_[0][0]}); } # Given a regular list of values, produce a finite stream sub list2stream { return Stream->empty unless @_; my @list = @_; my $h = shift @list; # print STDERR "list2stream @_\n"; return Stream->new($h, sub{&list2stream(@list)}); } ## Turn a stream into a regular Perl array ## Caution--only works on finite streams sub stream2list { my $s = shift; my @r; while (! $s->is_empty) { push @r, $s->head; $s = $s->tail; } @r; } ## Compute length of given stream sub length { my $s = shift; my $n = 0; while (! $s->is_empty) { $s = $s->tail; $n++; } $n; } 1;