<!-- category: functional -->
Mojo::Collection - Chainable Functional Pipelines in Perl
You know what people say about Perl and functional programming? Nothing. Because most of them don't know Perl has one of the slickest functional collection interfaces in any language. And it's been hiding in Mojolicious this whole time.That's Ruby-style chaining. Elixir-style pipelines. In Perl. Right now.use Mojo::Collection qw(c); my $result = c(1..10) ->grep(sub { $_ > 3 }) ->map(sub { $_ * 2 }) ->sort ->join(', '); say $result; # 10, 12, 14, 16, 18, 20, 8
Part 1: WHAT IS MOJO::COLLECTION?
Mojo::Collection is a chainable list type that ships with Mojolicious. You wrap any list of values inc() and suddenly you have method calls for filtering, transforming, sorting, and reducing.
Every method that returns a list returns another Mojo::Collection. That's the magic. You chain operations together, and each step feeds into the next.use Mojo::Collection qw(c); my $names = c('Alice', 'Bob', 'Charlie', 'Dave'); say $names->size; # 4 say $names->first; # Alice say $names->last; # Dave
No temporary variables. No intermediate arrays. Just a pipeline.
Input grep map sort join [1..10] -----> [4,5,6,7...] -> [8,10,12...] -> [8,10,...] -> "8, 10, ..." filter transform order stringify
Part 2: CREATING COLLECTIONS
Three ways to make one:You can also use the long form if you prefer:use Mojo::Collection qw(c); # from a list my $nums = c(1, 2, 3, 4, 5); # from an array my @data = ('perl', 'python', 'ruby'); my $langs = c(@data); # from a range my $range = c(1..100);
Butuse Mojo::Collection; my $stuff = Mojo::Collection->new('a', 'b', 'c');
c() is shorter and everyone uses it. Life is too short for Mojo::Collection->new.
Part 3: GREP - FILTERING
grep takes a sub and keeps elements where it returns true. Just like Perl's built-in grep, but chainable.
Filter with regex:my $big = c(1..20)->grep(sub { $_ > 15 }); say $big->join(', '); # 16, 17, 18, 19, 20
You can also pass a regex directly:my $words = c('apple', 'banana', 'avocado', 'cherry', 'apricot'); my $a_words = $words->grep(sub { $_ =~ m~^a~i }); say $a_words->join(', '); # apple, avocado, apricot
That's a nice shortcut. No sub needed for simple pattern matches.my $a_words = $words->grep(qr~^a~i); say $a_words->join(', '); # apple, avocado, apricot
Part 4: MAP - TRANSFORMING
map transforms every element. Again, like Perl's built-in, but chainable.
Extract data from complex structures:my $doubled = c(1..5)->map(sub { $_ * 2 }); say $doubled->join(', '); # 2, 4, 6, 8, 10
Transform strings:my $users = c( { name => 'Alice', age => 30 }, { name => 'Bob', age => 25 }, { name => 'Charlie', age => 35 }, ); my $names = $users->map(sub { $_->{name} }); say $names->join(', '); # Alice, Bob, Charlie
And here's a nice trick. You can pass a method name as a string, and Mojo::Collection will call that method on each element:my $shouting = c('hello', 'world')->map(sub { uc $_ }); say $shouting->join(' '); # HELLO WORLD
When your collection contains objects, this is incredibly clean.use Mojo::File qw(path); my $files = c(path('lib')->list->each); my $basenames = $files->map('basename');
Part 5: CHAINING - THE REAL POWER
Any single operation is just convenience. The power is in chaining them.Each step produces a new collection. The original is untouched. This is functional programming at its finest: no mutation, just transformation.my $result = c(1..50) ->grep(sub { $_ % 2 == 0 }) # evens only ->map(sub { $_ ** 2 }) # square them ->grep(sub { $_ < 500 }) # under 500 ->sort(sub { $b <=> $a }) # descending ->join(' > '); say $result; # 484 > 400 > 324 > 256 > 196 > 144 > 100 > 64 > 36 > 16 > 4
Compare that to the imperative version:
Six temporary variables. Six lines. And you have to read bottom-up to understand the flow if you nest them. The chained version reads top-down, step by step, like a recipe.my @nums = (1..50); my @evens = grep { $_ % 2 == 0 } @nums; my @squared = map { $_ ** 2 } @evens; my @small = grep { $_ < 500 } @squared; my @sorted = sort { $b <=> $a } @small; my $result = join(' > ', @sorted);
Part 6: EACH - ITERATION
each runs a sub for every element. Unlike map, it doesn't collect results. It's for side effects.
Output:c('apple', 'banana', 'cherry')->each(sub { my ($item, $index) = ($_, shift); say "$index: $item"; });
Note that1: apple 2: banana 3: cherry
each gives you a 1-based index as the first argument. The element is in $_.
You can also use each at the end of a chain to process the results:
Output:c(1..10) ->grep(sub { $_ % 3 == 0 }) ->map(sub { "divisible by 3: $_" }) ->each(sub { say $_ });
divisible by 3: 3 divisible by 3: 6 divisible by 3: 9
Part 7: FIRST, LAST, AND REDUCE
first returns the first element matching a condition:
Without a condition, it returns the first element:my $found = c(5, 12, 3, 18, 7)->first(sub { $_ > 10 }); say $found; # 12
say c('alpha', 'beta', 'gamma')->first; # alpha
last does the same from the other end:
say c('alpha', 'beta', 'gamma')->last; # gamma
reduce collapses a collection into a single value:
Build a hash from pairs:my $sum = c(1..10)->reduce(sub { $a + $b }); say $sum; # 55
Actually, for that you would just domy $pairs = c(name => 'Alice', age => 30, lang => 'Perl'); my $hash = $pairs->reduce(sub { my %h = ref $a eq 'HASH' ? %$a : ($a); $h{$b} = undef; \%h; });
my %h = @{$pairs}. But reduce shines for accumulation patterns that don't have a built-in shortcut.
# product of all elements my $product = c(1..5)->reduce(sub { $a * $b }); say $product; # 120 # longest string my $longest = c('cat', 'elephant', 'dog', 'hippopotamus')->reduce(sub { length($a) > length($b) ? $a : $b }); say $longest; # hippopotamus
Part 8: SORT, UNIQ, REVERSE, SHUFFLE
Sorting returns a new sorted collection:my $sorted = c(5, 3, 8, 1, 4)->sort; say $sorted->join(', '); # 1, 3, 4, 5, 8 # custom sort my $words = c('banana', 'apple', 'cherry'); my $by_length = $words->sort(sub { length($a) <=> length($b) }); say $by_length->join(', '); # apple, cherry, banana
uniq removes duplicates:
my $unique = c(1, 2, 2, 3, 3, 3, 4)->uniq; say $unique->join(', '); # 1, 2, 3, 4
reverse flips the order:
say c(1..5)->reverse->join(', '); # 5, 4, 3, 2, 1
shuffle randomizes:
Chain them all together:say c(1..10)->shuffle->join(', '); # different every time
my $result = c(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5) ->uniq ->sort ->reverse ->join(' -> '); say $result; # 9 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1
Part 9: REAL-WORLD EXAMPLES
Process a CSV-like structure:Build a frequency table:use Mojo::File qw(path); use Mojo::Collection qw(c); my $report = c(path('data.csv')->slurp =~ m~(.+)~g) # lines ->grep(sub { $_ !~ m~^#~ }) # skip comments ->map(sub { [split m~,~, $_] }) # split fields ->grep(sub { $_->[2] > 1000 }) # filter by value ->sort(sub { $a->[2] <=> $b->[2] }) # sort by value ->map(sub { "$_->[0]: \$$_->[2]" }) # format ->join("\n"); say $report;
Clean up a list of email addresses:my $words = c(split m~\s+~, path('book.txt')->slurp); my %freq; $words->map(sub { lc })->each(sub { $freq{$_}++ }); my $top10 = c(sort { $freq{$b} <=> $freq{$a} } keys %freq) ->head(10) ->map(sub { sprintf "%-15s %d", $_, $freq{$_} }) ->join("\n"); say $top10;
my $clean_emails = c(@raw_emails) ->map(sub { lc }) ->map(sub { s~\s+~~gr }) ->grep(sub { m~^[\w.+-]+\@[\w.-]+\.\w+$~ }) ->uniq ->sort ->join("\n");
Part 10: SLICING AND DICING
head and tail grab from either end:
say c(1..10)->head(3)->join(', '); # 1, 2, 3 say c(1..10)->tail(3)->join(', '); # 8, 9, 10
slice grabs specific indices:
say c('a'..'z')->slice(0, 4, 8, 14, 20)->join; # aeiou
compact removes undefined values:
my $clean = c(1, undef, 2, undef, 3)->compact; say $clean->join(', '); # 1, 2, 3
flatten un-nests collections:
my $nested = c(c(1, 2), c(3, 4), c(5, 6)); say $nested->flatten->join(', '); # 1, 2, 3, 4, 5, 6
Part 11: WHY THIS MATTERS
People keep saying Perl can't do functional programming. Or that it's ugly. Or that you need Haskell or Elixir for elegant data pipelines.Mojo::Collection proves them wrong. It gives you:
- Chainable operations that read top-to-bottom
- Immutable transforms where each step creates a new collection
- Lazy-feeling syntax that's actually eager but still clean
- Zero dependencies beyond Mojolicious (which you should be using anyway)
The built-in map, grep, and sort are great. But they don't chain. You either nest them (unreadable) or use temporary variables (verbose). Mojo::Collection gives you a third option that reads like prose.
You read it top to bottom. Get active users. Sort by name. Extract emails. Send newsletters. Done.# this tells a story c(@users) ->grep(sub { $_->{active} }) ->sort(sub { $a->{name} cmp $b->{name} }) ->map(sub { $_->{email} }) ->each(sub { send_newsletter($_) });
perl.ggc(@data) | grep -----> filter | map -----> transform | sort -----> order | join -----> stringify | result Data flows down. Logic reads down. Perl does functional. Deal with it.