File: blog-post-list-objects.mkdn

package info (click to toggle)
liblist-objects-withutils-perl 2.028003-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 1,292 kB
  • sloc: perl: 1,957; makefile: 17; sh: 6
file content (285 lines) | stat: -rw-r--r-- 7,994 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
Topic: Easy list ops with List-Objects-WithUtils
Date: 2013-03-26

Hmm.. I should sort these hash items. Okay, I've got my _sort_ and _map_
... but I want to sort these objects by `->name` ...
oh, right, _List::UtilsBy_!
But I want to iterate five of them at a time ... 
uh, hmm, is natatime in _List::Util_ or _List::MoreUtils_? 

Ah, screw it:

    use List::Objects::WithUtils;

    my $hash = hash(%previous);
    my $iter = $hash->values
                ->sort_by(sub { $_->name })
                ->natatime(5);

    while (my @objs = $iter->()) {
      ...
    }

But now I want to take a slice from
this hash and create a new hash ... ah, fook, what was the syntax again?
Hum. I could shove the keys I want in `@keys` ... but if any of those keys
don't exist in the old hash, I don't want them to be set to `undef` in the new
hash, so I need some kind of loop ...

Ah, never mind:

    my $newhash = $hash->sliced(qw/foo bar baz/);

There we are!

## A modern approach to list types

[List::Objects::WithUtils](http://metacpan.org/release/List-Objects-WithUtils)
exists to eliminate that whole train of thought by providing a
object-oriented interface to list types (arrays and hashes).

Aside from
providing native behavior like element manipulation, `sort`, `map`, 
`grep`, and so forth, you also get the most commonly useful utilities from
[List::Util](http://metacpan.org/module/List::Util),
[List::MoreUtils](http://metacpan.org/module/List::MoreUtils),
[List::UtilsBy](http://metacpan.org/module/List::UtilsBy),
and
[Syntax::Keyword::Junction](http://metacpan.org/module/Syntax::Keyword::Junction).

This post covers the raw basics and the things I use most frequently. See the
[List::Objects::WithUtils documentation on
metacpan](http://metacpan.org/release/List-Objects-WithUtils) for usage
details.

#### The basics

Getting going is pretty easy; where I might declare an array:

    my @array = qw/ a b c /;

... or an ARRAY:

    my $array = [qw/ a b c /];

I can just use `array()`:

    my $array = array(qw/ a b c /);

(Since these are ARRAY-type objects, code that previously treated $array as an
ARRAY ref will Just Work so long as it checks 'reftype' where most people
might use 'ref' -- porting my old code went pretty smoothly.)

Later on, if I need a plain list back out, I can get `all` elements:

    for my $item ($array->all) {
      ...
    }

... or perhaps just find out how many elements the array has:

    if ( $array->count > 2 ) {
      ...
    }

I can retrieve a specific index via `get`:

    my $second = $array->get(1);

... or change it via `set`:

    $array->set( 1, 'newval' );

The usual array operations work basically as-expected:

    $array->push('d');
    my $last  = $array->pop;

    $array->unshift('z');
    my $first = $array->shift;

I can `splice` or `delete` items:

    $array->delete(2);
    $array->splice( 0, 1 );

... and transform lists into strings via `join`:

    my $str = $array->join('');

Working with hashes is much the same; all the expected operations are
available. I can create my hash using the expected syntax:

    my $hash = hash(
      a => 'foo',
      b => 'bar',
      c => 'baz',
    );

Adding or setting hash elements works essentially as-expected:

    $hash->set(d => 'pie');

... but `set` can also take a sequence of pairs, which is great for
combining hashes:

    $hash->set(
      e => 'cake',
      f => 'banana',
    );

    # From a plain Perl hash:
    $hash->set( %old );
    
    # From a hash():
    $hash->set( $old->export );

Hash operations like `keys` and `values` return a list, which is of course
presented as an array() object.

That means it's easy to use chained operations to, say, sort by either key or
value:

    my @bykey = $hash->keys->sort->all;

    # By value, unique values only.
    my @byval = $hash->values->uniq->sort->all;

Since `sort` takes a sub, I could sort by a hash element, say:

    my $sorted = array(
      hash( foo => 1 ),
      hash( foo => 2 ),
      hash( foo => 3 ),
    )->sort(sub {
      $_[0]->get('foo') cmp $_[1]->get('foo')
    });

... but since `sort_by` is available, it would be much cleaner to do:

    $array->sort_by(sub { $_->get('foo') });

(Note the `$_` -- `sort_by` operates on a topicalizer.)

List operations returning new lists makes for pretty chaining:

    my @all = 
      $array->grep(sub { $_[0] =~ /foo/ })
            ->uniq
            ->sort
            ->map(sub { uc $_[0] })
            ->all;

How about a Schwartzian (with a little extra overhead, granted)?

    my $sorted = 
      array(qw/ abcd ab abc a /)
        ->map(sub { [ $_[0], length $_[0] ] })
        ->sort(sub { $_[0]->[1] <=> $_[1]->[1] })
        ->map(sub { $_[0]->[0] })

(This is pointless because we have `sort_by` available (see above), but it's a
well-known idiom with which we can demonstrate chaining.)

#### Junctions

One of the most common things I do with a list is apply `grep` to find stuff
-- but sometimes I'm not all that overly interested in _what_ I found, only
whether or not it is present.

I could, of course, use `grep` and check for found elements:

    if ( array(1 .. 10)->grep(sub { $_[0] == 4 })->has_any ) {
      ...
    }

... or I could turn to `has_any` or `first` for the same purpose, which could
be more efficient on account of terminating after the first successful hit:

    if ( array(1,2,3)->has_any(sub { $_ == 4 }) ) { }

    if ( array(1,2,3)->first(sub { $_ == 4}) )    { }

Still, this is a bit ugly; I just want to know if any items meet a certain
criteria, and typing 'sub { ... }' every five minutes gets to be silly.

Fortunately, the default `array` class happens to consume
[List::Objects::WithUtils::Role::WithJunctions](http://metacpan.org/module/List::Objects::WithUtils::Role::WithJunctions),
giving us easy access to
[Syntax::Keyword::Junction](http://metacpan.org/module/Syntax::Keyword::Junction)
goodness.

Calling `any_items` returns the overloaded `any` junction. Our check might
look more like:

    if ( array(1,2,3)->any_items == 4 ) {
      ...
    }

    if ( array('a', 'b', 'c')->any_items eq 'b' ) {
      ...
    }

You also get `all_items` for free:

    if ( array(1,2,3)->all_items > 0 ) {
      ...
    }

#### Slices

Traditional slice syntax isn't so bad when using a normal `@array` or `%hash`:

    my @array = ( 'a' .. 'z' );
    my @first = @array[0 .. 5];

    my %hash   = ( foo => 'bar', baz => 'foo', bar => 'baz' );
    # Values for wanted keys:
    my @foobar = @hash{'foo','bar'};

It starts to get a little more interesting when dealing with references:

    my @first  = @{ $array }[0 .. 5];
    my @foobar = @{ $hash }{'foo','bar'}

... and downright obnoxious when I'd like to turn a piece of a hash into a new
hash, for example:

    my %newhash = map {;
      exists $hash->{$_} ? ( $_ => $hash->{$_} ) : ()
    } 'foo', 'bar';

Instead of all that, I can just use `->sliced`, which works on both `array`
and `hash` objects. Now that array example looks more like this:

    my $array = array( 'a' .. 'z' );
    my $slice = $array->sliced( 0 .. 5 );

Unlike `->splice`, `->sliced` leaves the existing array alone and returns a
new array-type object containing the values requested.

A sliced() hash returns a new hash object:

    my $newhash = $hash->sliced('foo', 'bar');

When using `->sliced`, keys that don't exist in the old hash won't be created
with undefined values in the new hash.

In a similar vein, sometimes it's convenient to be able to get all the items
before the one matching some condition:

    my $before = $array->items_before(sub { $_ eq 'd' });

... or after it:

    my $after = $array->items_after(sub { $_ eq 't' });

We can make our hash manipulation rather shorter and a bit prettier:

    my $hash = hash( foo => 'bar', baz => 'foo', bar => 'baz' );

A get() that returns a list of values returns an array object:

    my @foobar  = $hash->get('foo', 'bar')->all;

There's plenty more. See the official documentation.