◈ Netsurfer / perl.gg / Mojo::Collection - Chainable Functional Pipelines in Perl

Mojo::Collection - Chainable Functional Pipelines in Perl

2026-03-12

This page shows how Mojo::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.
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
You get Ruby-style method chaining and Elixir-style pipelines directly in Perl.

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.
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 hands you back another 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.
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');
The short form 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.
my $big = c(1..20)->grep(sub { $_ > 15 }); say $big->join(', '); # 16, 17, 18, 19, 20
Filter with a regex pattern.
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 compiled regex directly.
my $a_words = $words->grep(qr~^a~i); say $a_words->join(', '); # apple, avocado, apricot
That shortcut removes the wrapper sub when you only need a simple pattern match.

Part 4: MAP - TRANSFORMING

map transforms every element. It behaves like Perl's built-in map, but it returns another collection.
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
You can also 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');
That pattern stays clean when your collection holds objects.

Part 5: CHAINING - THE REAL POWER

Any single operation is just convenience. The real value comes from chaining them together.
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 stays untouched. That is functional programming at its finest, since no mutation happens and only transformations flow through.

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

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

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; });
You would normally just do 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.
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 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.

# 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. You get active users, sort by name, extract emails, and send newsletters.
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
✓ Document: Donefunctional / 17.1 KiB▧