File Coverage

blib/lib/List/Stream.pm
Criterion Covered Total %
statement 97 124 78.2
branch 22 36 61.1
condition 6 13 46.1
subroutine 24 31 77.4
pod 14 14 100.0
total 163 218 74.7


line stmt bran cond sub pod time code
1             package List::Stream;
2              
3 8     8   862602 use strict;
  8         17  
  8         272  
4 8     8   69 use warnings;
  8         13  
  8         419  
5              
6 8     8   38 use Exporter qw(import);
  8         17  
  8         213  
7 8     8   49 use Carp;
  8         19  
  8         559  
8 8     8   48 use Scalar::Util qw(refaddr);
  8         24  
  8         13155  
9              
10             our @EXPORT = qw(stream);
11             our $VERSION = '0.0.3';
12              
13             # ABSTRACT: Simple, fast, functional processing of list data
14              
15             =pod
16              
17             =head1 Name
18              
19             L - Simple Java-like lazy, functionally-pure manipulation of lists.
20              
21             =head1 Synopsis
22              
23             L provides simple functionality for manipulating list data in a functional,
24             and simple way. L is lazy, meaning it stores all operations internally, so that they're only evaluated
25             when entirely necessary.
26              
27             =head1 Example
28              
29             use List::Stream;
30             use DBI;
31              
32             my $stream = stream DBI->selectall_array('SELECT * FROM users', {Slice => {}});
33              
34             # create a sub stream that maps all users to their role
35             my $mapped_stream = $stream->map(sub { $_->{role} });
36             # to_list applies all pending lazy operations, ie map.
37             my $number_of_users = $mapped_stream->filter(sub { $_ eq 'USER' })->count;
38             my $number_of_admins = $mapped_stream->filter(sub { $_ eq 'ADMIN' })->count;
39              
40             my %users_by_user_id = $stream->to_hash(sub { $_->{user_id} }, sub { $_ });
41              
42             my $it = $mapped_stream->to_iterator;
43              
44             =cut
45              
46             =pod
47              
48             =head2 stream
49              
50             Create a new L instance from a list.
51              
52             use List::Stream;
53             my @data = (1, 2, 3, 4);
54             my $stream = stream @data;
55              
56             =cut
57              
58             ## no critic [Subroutines::ProhibitSubroutinePrototypes]
59             sub stream (@) {
60 34     34 1 1515594 my (@values) = @_;
61 34         265 return List::Stream->_new( values => [@values] );
62             }
63              
64             =pod
65              
66             =head2 map
67              
68             Map data in a stream over a unary function. A -> B
69              
70             use List::Stream;
71             my @data = (1, 2, 3, 4);
72             my $stream = stream @data;
73             @data = $stream
74             ->map(sub { $_ + 1 })
75             ->to_list;
76             say @data; # 2, 3, 4, 5
77              
78             =cut
79              
80             sub map {
81 11     11 1 58 my ( $self, $mapper ) = @_;
82              
83 11 100       254 Carp::croak('Invalid operation provided to map, must be CODE')
84             unless ref($mapper) eq 'CODE';
85              
86             return $self->_add_op(
87             sub {
88 18     18   68 my $stream = shift;
89 18         31 my @accum;
90 18         60 push @accum, $mapper->($_) for ( $stream->_values->@* );
91 18         260 return stream @accum;
92             }
93 10         89 );
94             }
95              
96             =pod
97              
98             =head2 reduce
99              
100             Reduce data to a single element, via a bi-function, with the default accumlator passed as the second arg.
101             Retrieved by L. If the value reduced to is an ArrayRef, the streams data becomes
102             the ArrayRef.
103              
104             use List::Stream;
105             my @data = (1, 2, 3, 4);
106             my $stream = stream @data;
107             my $sum = $stream
108             ->reduce(sub {
109             my ($elem, $accum) = @_;
110             $accum += $elem;
111             }, 0) # pass default
112             ->first;
113             say $sum; # 10
114              
115             =cut
116              
117             sub reduce {
118 4     4 1 440 my ( $self, $reducer, $accum ) = @_;
119              
120 4 100       154 Carp::croak('Invalid operation provided to reduce, must be CODE')
121             unless ref($reducer) eq 'CODE';
122              
123 3 100       90 Carp::croak('No default/accumulator provided for reduce')
124             unless defined $accum;
125              
126             return $self->_add_op(
127             sub {
128 3     3   4 my $stream = shift;
129 3         4 my $a = $accum;
130 3         6 for my $val ( $stream->_values->@* ) {
131 9         25 $a = $reducer->( $val, $a );
132             }
133 3 50       12 return stream @$a if ref($a) eq 'ARRAY';
134 3         14 return stream $a;
135             }
136 2         10 );
137             }
138              
139             =pod
140              
141             =head2 filter
142              
143             Filters elements from the stream if they do not pass a predicate.
144              
145             use List::Stream;
146             my @data = (1, 2, 3, 4);
147             my $stream = stream @data;
148             @data = $stream
149             ->filter(sub { $_ >= 3 })
150             ->to_list;
151             say @data; # 3, 4
152              
153             =cut
154              
155             sub filter {
156 3     3 1 16 my ( $self, $filterer ) = @_;
157              
158 3 100       207 Carp::croak('Invalid operation provided to filter, must be CODE')
159             unless ref($filterer) eq 'CODE';
160              
161             return $self->_add_op(
162             sub {
163 2     2   5 my $stream = shift;
164 2         3 my @accum;
165              
166 2         18 for ( @{ $stream->_values } ) {
  2         9  
167 7 100       28 if ( $filterer->($_) ) {
168 1         3 push @accum, $_;
169             }
170             }
171              
172 2         9 return stream @accum;
173             }
174 2         32 );
175             }
176              
177             =pod
178              
179             =head2 flat_map
180              
181             Passes the contents of the stream to a mapping function, the mapping function must then return a L.
182              
183             use List::Stream;
184             my @data = (1, 2, 3, 4);
185             my $stream = stream @data;
186             @data = $stream
187             ->flat_map(sub {
188             stream(@_)->map(sub { $_ * 2 })
189             })
190             ->to_list;
191             say @data; # 2, 4, 6, 8
192              
193             =cut
194              
195             sub flat_map {
196 0     0 1 0 my ( $self, $mapper ) = @_;
197              
198 0 0       0 Carp::croak('Invalid operation provided to flat_map, must be CODE')
199             unless ref($mapper) eq 'CODE';
200              
201             return $self->_add_op(
202             sub {
203 0     0   0 my $stream = shift;
204 0         0 my $new_stream = $mapper->( @{ $stream->_values } );
  0         0  
205 0 0       0 Carp::croak(
206             'Expected $mapper to return List::Stream, instead got a '
207             . ref($new_stream) )
208             unless ref($new_stream) eq 'List::Stream';
209 0         0 return $new_stream;
210             }
211 0         0 );
212             }
213              
214             =pod
215              
216             =head2 unique
217              
218             Filters the stream down to only unique values. This uses a HASH to determine uniqueness.
219              
220             use List::Stream;
221             my $stream = stream qw(a a b c b d e);
222             my @values = $stream->unique->to_list;
223             say @values; # a, b, c, d, e
224              
225             If you'd like to use another value to represent the value in the uniquness check you can pass a sub-routine
226             that will be passed the value, and the result of the sub-routine will be the uniqueness identifier.
227              
228             use List::Stream;
229             my $stream = stream ({ id => 123 }, { id => 456 }, { id => 123 });
230             my @values = $stream->unique(sub { $_->{id} })->to_list;
231             say @values; # { id => 123 }, { id => 456 }
232              
233             =cut
234              
235             sub unique {
236 2     2 1 1849 my ( $self, $mapper ) = @_;
237              
238 2 100       7 if ($mapper) {
239 1 50       6 Carp::croak('Invalid operation passed to unique, must be CODE')
240             unless ref($mapper) eq 'CODE';
241             }
242              
243 2   66 6   15 $mapper //= sub { shift };
  6         11  
244              
245             return $self->_add_op(
246             sub {
247 3     3   7 my $stream = shift;
248 3         26 my %vals;
249             my @accum;
250              
251 3         5 for ( @{ $stream->_values } ) {
  3         7  
252 9         20 my $unique_value = $mapper->($_);
253              
254 9 100       25 if ( ref($unique_value) ) {
255 6         12 $unique_value = refaddr $unique_value;
256             }
257              
258 9 100       22 if ( exists $vals{$unique_value} ) {
259 1         4 next;
260             }
261              
262 8         16 $vals{$unique_value} = 1;
263 8         19 push @accum, $_;
264             }
265              
266 3         8 return stream @accum;
267             }
268 2         12 );
269             }
270              
271             =pod
272              
273             =head2 skip
274              
275             Skips C elements in the stream, discarding them.
276              
277             use List::Stream;
278             my @data = (1, 2, 3, 4);
279             my $stream = stream @data;
280             @data = $stream
281             ->skip(2)
282             ->to_list;
283             say @data; # 3, 4
284              
285             =cut
286              
287             sub skip {
288 0     0 1 0 my ( $self, $n_to_skip ) = @_;
289              
290             return $self->_add_op(
291             sub {
292 0     0   0 my $stream = shift;
293 0         0 my @values = @{ $stream->_values };
  0         0  
294 0         0 shift @values for ( 0 .. ( $n_to_skip - 1 ) );
295 0         0 return stream @values;
296             }
297 0         0 );
298             }
299              
300             =pod
301              
302             =head2 for_each
303              
304             Applies a void context unary-function to the stream.
305              
306             use List::Stream;
307             my @data = (1, 2, 3, 4);
308             my $stream = stream @data;
309             $stream->for_each(sub { say $_; }); # says 1, then 2, then 3, then 4
310              
311             =cut
312              
313             sub for_each {
314 0     0 1 0 my ( $self, $each ) = @_;
315 0 0       0 Carp::croak('Invalid operation provided to for_each, must be CODE')
316             unless ref($each) eq 'CODE';
317 0         0 my @elems = $self->to_list;
318 0         0 $each->($_) for @elems;
319             }
320              
321             =pod
322              
323             =head2 to_list
324              
325             Applies all pending operations on the stream, and collects them to an array.
326              
327             my @data = (1, 2, 3, 4);
328             my $stream = stream(@data)->map(sub { $_ + 1 });
329             # The mapping hasn't happened yet, we're lazy.
330             @data = $stream->to_list;
331             say @data; # 2, 3, 4, 5
332              
333             =cut
334              
335             sub to_list {
336 16     16 1 41 my ($self) = @_;
337 16         32 return @{ $self->_collect( [] ) };
  16         54  
338             }
339              
340             =pod
341              
342             =head2 to_hash
343              
344             Applies all pending operations on the stream, and collects them to a hash.
345              
346             my @data = (1, 2, 3, 4);
347             my $stream = stream(@data)->map(sub { $_ + 1 });
348             # The mapping hasn't happened yet, we're lazy.
349             my %hash = $stream->to_hash;
350             say %hash; # 2 => 3, 4 => 5
351              
352             You may also provide a key, and value mapper to be applied to each element.
353              
354             my @data = (1, 2, 3, 4);
355             my $stream = stream(@data)->map(sub { $_ + 1 });
356             my %hash = $stream->to_hash(sub { $_ * 2 }, sub { $_ });
357             say %hash; # 4 => 2, 6 => 3, 8 => 4, 10 => 5
358              
359             =cut
360              
361             sub to_hash {
362 1     1 1 4 my ( $self, $key_mapper, $value_mapper ) = @_;
363              
364 1 50 33     8 if ( $key_mapper && $value_mapper ) {
365 1 50 33     7 Carp::croak(
366             'Key or value mapper provided to "to_hash" are not CODE refs.')
367             unless ref($key_mapper) eq 'CODE' && ref($value_mapper) eq 'CODE';
368              
369 1         4 my @data = $self->to_list;
370 1         2 my %ret;
371              
372 1         3 for (@data) {
373 3         26 $ret{ $key_mapper->($_) } = $value_mapper->($_);
374             }
375              
376 1         13 return %ret;
377             }
378              
379 0         0 return %{ $self->_collect( {} ) };
  0         0  
380             }
381              
382             =pod
383              
384             =head2 first
385              
386             Gets the first element of the stream, and applies all pending operations.
387             This is useful when using C, when you've reduced to a single value.
388              
389             use List::Stream;
390             my @data = (1, 2, 3, 4);
391             my $stream = stream(@data)->map(sub { $_ + 1 });
392             my $first = $stream->first;
393             say $first; # 2
394              
395             Since reduce reduces the stream to a single element, C can be used to get the reduced value.
396              
397             use List::Stream;
398             my @data = (1, 2, 3, 4);
399             my $stream = stream(@data)
400             ->map(sub { $_ + 1 })
401             ->reduce(sub { my ($elem, $accum) = @_; $elem += $accum }, 0);
402             my $first = $stream->first;
403             say $first; # 14
404              
405             =cut
406              
407             sub first {
408 2     2 1 11 my ($self) = @_;
409 2         7 my @values = $self->to_list;
410 2         12 return $values[0];
411             }
412              
413             =pod
414              
415             =head2 is_empty
416              
417             Applies all pending operations, and returns true if the stream is empty or
418             false if the stream has at least one value.
419              
420             use List::Stream;
421             my $stream = stream(1,2,3,4);
422             say $stream->filter(sub { $_ > 5 })->is_empty; # true
423              
424             =cut
425              
426             sub is_empty {
427 2     2 1 9 my ($self) = @_;
428 2         10 return $self->count == 0;
429             }
430              
431             =pod
432              
433             =head2 to_iterator
434              
435             Applies all pending operations on the stream, and returns an iterator in the form of a sub-routine.
436              
437             use List::Stream;
438             my $stream = stream(qw(a b c d e f g))
439             ->map(sub { $_ . 'f' });
440             my $it = $stream->to_iterator;
441             while (my $val = $it->()) {
442             say $val;
443             }
444              
445             =cut
446              
447             sub to_iterator {
448 0     0 1 0 my ($self) = @_;
449 0         0 my @values = $self->to_list;
450             return sub {
451             ## no critic [Subroutines::ProhibitExplicitReturnUndef]
452 0 0   0   0 return undef if !@values;
453 0         0 return shift @values;
454             }
455 0         0 }
456              
457             =pod
458              
459             =head2 count
460              
461             Applies all pending operations on the stream, and returns the count of elements in the stream.
462              
463             my $stream = stream(qw(a b c d e f g))
464             ->map(sub { $_ . 'f' });
465             my $length = $stream->length;
466             say $length; # 7
467              
468             =cut
469              
470             sub count {
471 8     8 1 1416 my ($self) = @_;
472 8         38 return scalar $self->to_list;
473             }
474              
475             sub _collect {
476 16     16   40 my ( $self, $type ) = @_;
477              
478 16         35 my @ops = @{ $self->{ops} };
  16         60  
479 16         78 while ( my $op = shift @ops ) {
480 26         66 $self = $op->($self);
481             }
482              
483 16         49 my $ret = $self->_values;
484 16 50       70 if ( ref($type) eq 'HASH' ) {
485 0         0 return {@$ret};
486             }
487              
488 16         166 return $ret;
489             }
490              
491             sub _values {
492 43     43   936 my ($self) = @_;
493 43         134 return $self->{values};
494             }
495              
496             sub _add_op {
497 16     16   37 my ( $self, $op ) = @_;
498              
499 16 50       56 Carp::croak('Invalid operation provided, must be CODE')
500             unless ref($op) eq 'CODE';
501              
502 16         36 push @{ $self->{ops} }, $op;
  16         62  
503              
504 16         85 return $self;
505             }
506              
507             sub _new {
508 34     34   136 my ( $class, %args ) = @_;
509             return bless {
510             values => ( $args{values} // [] ),
511 34   50     419 ops => ( $args{ops} // [] )
      50        
512             }, $class;
513             }
514              
515             1;