perl.gg / functional

<!-- category: functional -->

Mojo::Collection - Chainable Functional Pipelines in Perl

2026-03-12

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.
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
That's Ruby-style chaining. Elixir-style pipelines. In Perl. Right now.

Part 1: WHAT IS MOJO::COLLECTION?

Mojo::Collection is a chainable list type that ships with Mojolicious. You wrap any list of values in c() and suddenly you have method calls for filtering, transforming, sorting, and reducing.
use Mojo::Collection qw(c); my $names = c('Alice', 'Bob', 'Charlie', 'Dave'); say $names->size; # 4 say $names->first; # Alice say $names->last; # Dave
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.

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:
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);
You can also use the long form if you prefer:
use Mojo::Collection; my $stuff = Mojo::Collection->new('a', 'b', 'c');
But 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.
my $big = c(1..20)->grep(sub { $_ > 15 }); say $big->join(', '); # 16, 17, 18, 19, 20
Filter with regex:
my $words = c('apple', 'banana', 'avocado', 'cherry', 'apricot'); my $a_words = $words->grep(sub { $_ =~ m~^a~i }); say $a_words->join(', '); # apple, avocado, apricot
You can also pass a regex directly:
my $a_words = $words->grep(qr~^a~i); say $a_words->join(', '); # apple, avocado, apricot
That's a nice shortcut. No sub needed for simple pattern matches.

Part 4: MAP - TRANSFORMING

map transforms every element. Again, like Perl's built-in, but chainable.
my $doubled = c(1..5)->map(sub { $_ * 2 }); say $doubled->join(', '); # 2, 4, 6, 8, 10
Extract data from complex structures:
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
Transform strings:
my $shouting = c('hello', 'world')->map(sub { uc $_ }); say $shouting->join(' '); # HELLO WORLD
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:
use Mojo::File qw(path); my $files = c(path('lib')->list->each); my $basenames = $files->map('basename');
When your collection contains objects, this is incredibly clean.

Part 5: CHAINING - THE REAL POWER

Any single operation is just convenience. The power is in chaining them.
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
Each step produces a new collection. The original is untouched. This is functional programming at its finest: no mutation, just transformation.

Compare that to the imperative version:

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

Part 6: EACH - ITERATION

each runs a sub for every element. Unlike map, it doesn't collect results. It's for side effects.
c('apple', 'banana', 'cherry')->each(sub { my ($item, $index) = ($_, shift); say "$index: $item"; });
Output:
1: apple 2: banana 3: cherry
Note that 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:

c(1..10) ->grep(sub { $_ % 3 == 0 }) ->map(sub { "divisible by 3: $_" }) ->each(sub { say $_ });
Output:
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:
my $found = c(5, 12, 3, 18, 7)->first(sub { $_ > 10 }); say $found; # 12
Without a condition, it returns the first element:
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:
my $sum = c(1..10)->reduce(sub { $a + $b }); say $sum; # 55
Build a hash from pairs:
my $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; });
Actually, for that you would just do 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:
say c(1..10)->shuffle->join(', '); # different every time
Chain them all together:
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:
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;
Build a frequency table:
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;
Clean up a list of email addresses:
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:

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.

# this tells a story c(@users) ->grep(sub { $_->{active} }) ->sort(sub { $a->{name} cmp $b->{name} }) ->map(sub { $_->{email} }) ->each(sub { send_newsletter($_) });
You read it top to bottom. Get active users. Sort by name. Extract emails. Send newsletters. Done.
c(@data) | grep -----> filter | map -----> transform | sort -----> order | join -----> stringify | result Data flows down. Logic reads down. Perl does functional. Deal with it.
perl.gg