File Coverage

blib/lib/List/Lazy.pm
Criterion Covered Total %
statement 153 153 100.0
branch 32 36 88.8
condition 16 17 94.1
subroutine 35 35 100.0
pod 13 14 92.8
total 249 255 97.6


line stmt bran cond sub pod time code
1             package List::Lazy;
2             our $AUTHORITY = 'cpan:YANICK';
3             # ABSTRACT: Generate lists lazily
4             $List::Lazy::VERSION = '0.3.1';
5              
6              
7              
8 8     8   643208 use Moo;
  8         73323  
  8         31  
9 8     8   11790 use MooX::HandlesVia;
  8         59687  
  8         36  
10              
11 8     8   3491 use Clone qw/ clone /;
  8         16024  
  8         374  
12              
13 8     8   105 use 5.22.0;
  8         20  
14              
15 8     8   2875 use experimental 'signatures', 'postderef';
  8         21611  
  8         38  
16              
17 8     8   4857 use List::MoreUtils;
  8         78666  
  8         41  
18 8     8   5968 use Carp;
  8         15  
  8         12516  
19              
20             *list_before = *List::MoreUtils::before;
21              
22             extends 'Exporter::Tiny';
23              
24             sub _exporter_validate_opts {
25 8     8   1155     my $into = $_[1]->{into};
26 8 50       541     eval qq{
27             *${into}::a = *::a;
28             *${into}::b = *::b;
29             1;
30             } or die $@;
31             }
32              
33              
34             our @EXPORT_OK = qw/ lazy_list lazy_range lazy_fixed_list /;
35              
36 10     10   11 sub _lazy_list ($generator,$state=undef) {
  10         12  
  10         10  
  10         11  
37 10         152     return List::Lazy->new(
38                     generator => $generator,
39                     state => $state,
40                 );
41             }
42              
43 10     10 1 24 sub lazy_list :prototype(&@) { goto &_lazy_list }
44              
45 8     8   23 sub _lazy_range ($min,$max,$step=1) {
  8         11  
  8         10  
  8         9  
  8         8  
46 8 100   26   26     my $it = ref $step ? $step : sub { $_ + $step };
  26         32  
47              
48                 return scalar lazy_list {
49 33 100 100 33   76         return if defined $max and $_ > $max;
50 29         32         my $current = $_;
51 29         42         $_ = $it->();
52 29         69         return $current;
53 8         33     } $min;
54             }
55              
56              
57 8     8 1 3309 sub lazy_range :prototype($$@) { goto &_lazy_range }
58              
59             sub lazy_fixed_list {
60 12     12 1 5801     my @list = @_;
61                 return List::Lazy->new(
62                     _next => [ @list ],
63                     is_done => 0,
64 12     12   19         generator => sub { return () },
65 12         200     );
66             }
67              
68             has generator => (
69                 is => 'ro',
70                 required => 1,
71             );
72              
73             has state => (
74                 is => 'rw'
75             );
76              
77             has _next => (
78                 is => 'rw',
79                 handles_via => 'Array',
80                 handles => {
81                     has_next => 'count',
82                     shift_next => 'shift',
83                     push_next => 'push',
84                     _all_next => 'elements',
85                 },
86                 default => sub { [] },
87             );
88              
89             has is_done => (
90                 is => 'rw',
91                 default => sub { 0 },
92             );
93              
94 127     127 0 4655 sub generate_next($self) {
  127         140  
  127         125  
95 127         186     local $_ = $self->state;
96              
97 127         197     my @values = $self->generator->();
98 127         220     $self->state($_);
99              
100 127 100       217     $self->is_done(1) unless @values;
101              
102 127         1766     return @values;
103             }
104              
105 126     126 1 1509 sub next($self,$num=1) {
  126         133  
  126         134  
  126         124  
106 126         123     my @returns;
107              
108 126 50 100     378     croak "next called in scalar context with \$num = $num"
      66        
109                     if defined wantarray and not wantarray and $num != 1;
110              
111 126   100     376     while( @returns < $num and not $self->is_done ) {
112 196 100       4954         $self->push_next( $self->generate_next )
113                         unless $self->has_next;
114              
115 196 100       9227         next unless $self->has_next;
116              
117 172 100       6185         if( ref $self->_next->[0] eq 'List::Lazy' ) {
118 9         14             my $list = $self->_next->[0];
119 9         16             push @returns, $list->next;
120 9 100       67             $self->shift_next if $list->is_done;
121                     }
122                     else {
123 163         2063             push @returns, $self->shift_next;
124                     }
125                 }
126              
127 126 100       4801     return wantarray ? @returns : $returns[0];
128             }
129              
130 8     8 1 62 sub all ($self) {
  8         12  
  8         11  
131 8         18     return $self->next(1E99);
132             }
133              
134 1     1 1 1406 sub reduce($self,$reducer,$reduced=undef) {
  1         2  
  1         2  
  1         2  
  1         1  
135 1 50       6     $reduced = $self->next if @_ < 3;
136              
137 1         3     while( my $next = $self->next ) {
138 9         11         local ( $::a, $::b ) = ( $reduced, $next );
139 9         16         $reduced = $reducer->();
140                 }
141              
142 1         4     return $reduced;
143             }
144              
145 7     7 1 27 sub map($self,$map) {
  7         8  
  7         10  
  7         7  
146                 return List::Lazy->new(
147                     state => $self->_clone,
148                     generator => sub {
149 25     25   65             while( my @next = $_->next ) {
150 29         45                 @next = map { $map->() } @next;
  29         45  
151 29 100       125                 return @next if @next;
152                         }
153 2         4             return;
154                     },
155 7         16     );
156             }
157              
158 1     1 1 1236 sub batch($self,$n) {
  1         2  
  1         1  
  1         2  
159                 return List::Lazy->new(
160                     state => [ $self->_clone, [] ],
161                     generator => sub {
162 4     4   5             my $stash = $_->[1];
163              
164 4         7             while( my @next = $_->[0]->next ) {
165 5         8                 push @$stash, @next;
166 5 100       11                 if( @$stash >= $n ) {
167 2         7                     return [ splice @$stash, 0, $n ];
168                             }
169                         }
170              
171 2 100       7             return @$stash ? [ splice @$stash ] : ();
172                     },
173 1         2     );
174             };
175              
176 2     2 1 15 sub grep($self,$filter) {
  2         3  
  2         2  
  2         3  
177 14 100   14   19     $self->map(sub{ $filter->() ? $_ : () })
178 2         7 }
179              
180 2     2 1 2160 sub spy($self,$sub=undef) {
  2         4  
  2         3  
  2         2  
181 2   100 1   10     $sub ||= sub { carp $_ };
  1         16  
182 2     6   7     $self->map(sub{ $sub->(); $_ } );
  6         23  
  6         760  
183             }
184              
185 18     18   19 sub _clone($self,%args) {
  18         22  
  18         20  
  18         19  
186 18         401     return List::Lazy->new(
187                     state => clone( $self->state ),
188                     generator => $self->generator,
189                     _next => [ $self->_next->@* ],
190                     %args
191                 );
192             }
193              
194 1     1 1 1317 sub until($self,$condition) {
  1         2  
  1         2  
  1         1  
195 1         2     my $done;
196                 return List::Lazy->new(
197                     state => $self->_clone,
198                     generator => sub {
199 11 50   11   17             return () if $done;
200 11         19             my @next = $_->next;
201 11         37             my @filtered = list_before( sub { $condition->() }, @next );
  11         21  
202 11         41             $done = @filtered < @next;
203 11         18             return @filtered;
204                     },
205 1         3     );
206             }
207              
208 4     4 1 54 sub append($self,@list) {
  4         5  
  4         6  
  4         4  
209              
210                 return List::Lazy->new(
211 9         27         state => [ map { $_->_clone } $self, @list ],
212                     generator => sub {
213 36     36   57             while(@$_) {
214 45   100     163                 shift @$_ while @$_ and $_->[0]->is_done;
215 45 100       69                 last unless @$_;
216 41         55                 my @next = $_->[0]->next;
217 41 100       88                 return @next if @next;
218                         }
219 4         7             return ();
220                     },
221 4         6     );
222              
223             }
224              
225 1     1 1 16 sub prepend( $self, @list ) {
  1         2  
  1         2  
  1         1  
226 1         2     push @list, $self;
227 1         1     $self = shift @list;
228 1         3     $self->append(@list);
229             }
230              
231             1;
232              
233             __END__
234            
235             =pod
236            
237             =encoding UTF-8
238            
239             =head1 NAME
240            
241             List::Lazy - Generate lists lazily
242            
243             =head1 VERSION
244            
245             version 0.3.1
246            
247             =head1 SYNOPSIS
248            
249             use List::Lazy qw/ lazy_range /;
250            
251             my $range = lazy_range( 1, undef )->grep(sub{ $_ % 2})->map( sub { '!' x $_ } );
252            
253             say $_ for $range->next(3); # prints ! !!! !!!!!
254            
255             =head1 DESCRIPTION
256            
257             C<List::Lazy> creates lists that lazily evaluate their next values on-demand.
258            
259             =head1 EXPORTED FUNCTIONS
260            
261             Lazy::List doesn't export any function by default, but will export the three following
262             functions on request.
263            
264             =head2 lazy_list
265            
266             my $list = lazy_list $generator_sub, $state;
267            
268             A convenience shortcut for the List::Lazy constructor. The C<$state> will be available
269             (and can be changed) by the generator subroutine. The generator subroutine is expected
270             to return a list of one or more next items of the list. Returning an empty list means
271             that the list has reached its end.
272            
273             my $even_numbers = lazy_list { $_ += 2 } 0; # will return 2, 4, 6, ...
274            
275             In additional of regular values, the generator can also return lazy lists,
276             which will be seamlessly expanded.
277            
278             my $list = lazy_range( 1, undef )->map(sub { lazy_range( 1, $_ ) });
279             # will return 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, ...
280            
281             =head2 lazy_range
282            
283             my $range = lazy_range $min, $max, $iterator;
284            
285             Creates a list iterating over a range of values. C<$min> and C<$max> are required, but C<$max> can be
286             C<undef> (meaning no upper limit). The C<$iterator> is optional and defaults to the value C<1>.
287             The C<$iterator> can be a number, which will be the step at which the numbers are increased, or a coderef that will
288             be passed the previous value as C<$_>, and is expected to return the next value.
289            
290             my $palinumbers = lazy_range 99, undef, sub { do { $_++ } until $_ eq reverse $_; $_ };
291            
292             say join ' ', $palinumbers->next(3); # 99 101 111
293            
294             =head2 lazy_fixed_list
295            
296             my $list = lazy_fixed_list @some_array;
297            
298             Creates a lazy list that will returns the values of the given array.
299            
300             =head1 CLASS
301            
302             =head2 new
303            
304             my $list = List::Lazy->new(
305             state => 1,
306             generator => sub {
307             $_++;
308             },
309             );
310            
311             Creates a lazy list.
312            
313             =head3 arguments
314            
315             =over state
316            
317             The state will be passed to the generator as C<$_>. If it is modified by the generator,
318             its new value will be saved for the next invocation.
319            
320             =over generator
321            
322             A coderef that generates one or more next items for the list. If it returns an empty list,
323             the stream will be considered to be exhausted.
324            
325             =back
326            
327             =head2 is_done
328            
329             Returns C<true> is the list is exhausted.
330            
331             =head2 next($num)
332            
333             Returns the next C<$num> items of the list (or less if the list doesn't have
334             that many items left). C<$num> defaults to C<1>.
335            
336             my $range = lazy_range 1, 100;
337            
338             while( my $next = $range->next ) {
339             ...
340             }
341            
342             =head2 reduce
343            
344             my $value = $list->reduce( $reducing_sub, $initial_value );
345            
346             Iterates through the list and reduces its values via the C<$reducing_sub>, which
347             will be passed the cumulative value and the next item via C<$a> and C<$b>.
348             If C<$initial_value> is not given, it defaults to the first element of the list.
349            
350             my $sum = lazy_range( 1, 100 )->reduce( sub { $a + $b } );
351            
352             =head2 batch
353            
354             my $new_list = $list->batch($n);
355            
356             Creates a new list where the items of the original list are batched in groups
357             of C<$n> (or less for the last batch).
358            
359             my $list = lazy_fixed_list( 1..100 )->batch(3);
360            
361             my $x = $list->next; # $x == [ 1, 2, 3]
362            
363             =head2 map
364            
365             my $new_list = $list->map( $mapper_sub );
366            
367             Creates a new list by applying the transformation given by C<$mapper_sub> to the
368             original list. The sub ill be passed the original next item via C<$_>
369             and is expected to return its transformation, which
370             can modify the item, explode it into many items, or suppress it,
371            
372             Note that the new list do a deep clone of the original list's state, so reading
373             from the new list won't affect the original list.
374            
375             my $recount = ( lazy_range 1, 100 )->map( sub { 1..$_ } );
376             # will return 1 1 2 1 2 3 1 2 3 4 ...
377            
378             =head2 grep
379            
380             my $new_list = $list->grep( $filter_sub );
381            
382             Creates a new list by applying the filtering given by C<$filter_sub> to the
383             original list. The sub will be passed the original next item via C<$_>
384             and is expected to return a boolean indicating if the item should be kept or not.
385            
386             Note that the new list do a deep clone of the original list's state, so reading
387             from the new list won't affect the original list.
388            
389             my $odd = ( lazy_range 1, 100 )->grep( sub { $_ % 2 } );
390            
391             =head2 spy
392            
393             my $new_list = $list->spy( $sub );
394            
395             Creates a new list that will execute the spy C<$sub> for
396             every value it sees (with the value assigned to C<$_>).
397            
398             If C<$sub> is not given, it'll C<carp> the values.
399            
400             =head2 until
401            
402             my $new_list = $list->until( $condition );
403            
404             Creates a new list that truncates the original list as soon
405             as the condition is met.
406            
407             my $to_ten = $list->until(sub{ $_ > 10 });
408            
409             =head2 append
410            
411             my $new_list = $list->append( @other_lists );
412            
413             Creates a new list that will return first the elements of C<$list>,
414             and those of the C<@other_lists>.
415            
416             Note that the new list do a deep clone of the original lists's state, so reading
417             from the new list won't affect the original lists.
418            
419             my $range = lazy_range 1..100;
420             my $twice = $range->append( $range );
421            
422             =head2 prepend
423            
424             my $new_list = $list->prepend( @other_lists );
425            
426             Like C<append>, but prepend the other lists to the current one.
427            
428             Note that the new list do a deep clone of the original lists's state, so reading
429             from the new list won't affect the original lists.
430            
431             =head2 all
432            
433             my @rest = $list->all;
434            
435             Returns all the remaining values of the list. Be careful: if the list is unbounded,
436             calling C<all()> on it will result into an infinite loop.
437            
438             =head1 AUTHOR
439            
440             Yanick Champoux <yanick@babyl.dyndns.org>
441            
442             =head1 COPYRIGHT AND LICENSE
443            
444             This software is copyright (c) 2018, 2017, 2016 by Yanick Champoux.
445            
446             This is free software; you can redistribute it and/or modify it under
447             the same terms as the Perl 5 programming language system itself.
448            
449             =cut
450