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.
|