Mojo::Collection - Chainable Functional Pipelines in Perl
This page shows howMojo::Collection turns Perl lists into chainable functional pipelines. You wrap values in c() and get filtering, mapping, sorting, and reducing without temporary variables. It ships inside Mojolicious, so you probably already have it.
You get Ruby-style method chaining and Elixir-style pipelines directly in Perl.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 in c() and get method calls for filtering, transforming, sorting, and reducing.
Every method that returns a list hands you back anotheruse Mojo::Collection qw(c); my $names = c('Alice', 'Bob', 'Charlie', 'Dave'); say $names->size; # 4 say $names->first; # Alice say $names->last; # Dave
Mojo::Collection. You chain operations together and each step feeds directly into the next. You skip temporary variables and intermediate arrays for a single 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
There are three standard ways to build 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);
The short formuse Mojo::Collection; my $stuff = Mojo::Collection->new('a', 'b', 'c');
c() is the standard, so most people just use it.
Part 3: GREP - FILTERING
grep takes a code block and keeps elements where it returns true. It works like Perl's built-in grep, but it returns a chainable collection.
Filter with a regex pattern.my $big = c(1..20)->grep(sub { $_ > 15 }); say $big->join(', '); # 16, 17, 18, 19, 20
You can also pass a compiled 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 shortcut removes the wrapper sub when you only need a simple pattern match.my $a_words = $words->grep(qr~^a~i); say $a_words->join(', '); # apple, avocado, apricot
Part 4: MAP - TRANSFORMING
map transforms every element. It behaves like Perl's built-in map, but it returns another collection.
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
You can also pass a method name as a string, andmy $shouting = c('hello', 'world')->map(sub { uc $_ }); say $shouting->join(' '); # HELLO WORLD
Mojo::Collection will call that method on each element.
That pattern stays clean when your collection holds objects.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 real value comes from chaining them together.Each step produces a new collection. The original stays untouched. That is functional programming at its finest, since no mutation happens and only transformations flow through.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 and six lines of code. You also have to read bottom up to follow the flow if you nest them. The chained version reads top down, step by step.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 code block for every element. Unlike map, it does not collect results, so you use it 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 one based index as the first argument while the element sits in $_.
You can also use each at the end of a chain to process the final 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
You would normally 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} for that case. reduce shines for accumulation patterns that lack 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 cannot do functional programming, or that it looks ugly, or that you need Haskell or Elixir for elegant data pipelines.Mojo::Collection proves them wrong by giving you chainable operations that read top to bottom. It also hands you immutable transforms where each step creates a new collection, lazy feeling syntax that stays eager, and zero dependencies beyond Mojolicious.
The built in map, grep, and sort are fine, but they do not chain. You either nest them until the code becomes unreadable or you rely on temporary variables. Mojo::Collection gives you a third option that reads like a recipe.
You read it top to bottom. You get active users, sort by name, extract emails, and send newsletters.# 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.