perl.gg / functional

<!-- category: functional -->

The /e and /r Combo in Map

2026-03-29

Modify copies, not originals. Evaluate code in the replacement. Do both at once.
my @doubled = map { s~(\d+)~$1 * 2~er } @prices;
That line takes every element of @prices, finds every number, doubles it, and returns the modified copy. The original @prices is untouched.

Two flags do the heavy lifting. The /e flag treats the replacement side as Perl code and evaluates it. The /r flag returns a modified copy instead of mutating the original. Put them together in a map and you get safe, functional transformations with computational power in the replacement.

Part 1: THE /r FLAG REFRESHER

Normally s~~~ modifies the variable it's bound to and returns the substitution count:
my $text = "price: 50"; my $count = ($text =~ s~50~100~); # $count is 1, $text is now "price: 100"
With /r, it returns the modified string and leaves the original alone:
my $text = "price: 50"; my $new = ($text =~ s~50~100~r); # $new is "price: 100", $text is still "price: 50"
The "r" stands for non-destructive (the docs say "return"). The variable is never touched. You get a fresh copy.

Part 2: THE /e FLAG REFRESHER

Normally the replacement side of s~~~ is a string:
my $text = "count: 5"; $text =~ s~5~10~; # literal replacement
With /e, the replacement is evaluated as Perl code:
my $text = "count: 5"; $text =~ s~(\d+)~$1 * 2~e; # $text is now "count: 10"
The captured $1 contains "5". The expression $1 * 2 evaluates to 10. The match is replaced with the result of the evaluation.

This is arbitrary Perl code. Function calls, conditionals, math, whatever you want:

$text =~ s~(\w+)~uc($1)~eg; # uppercase every word $text =~ s~(\d+)~sprintf("%04d", $1)~eg; # zero-pad numbers $text =~ s~(error|warn)~"[$1]"~egi; # bracket keywords

Part 3: COMBINING /e AND /r

Put them together and you get evaluated replacements that return copies:
my $original = "Item costs 25 dollars"; my $inflated = ($original =~ s~(\d+)~$1 * 1.15~er); # $original: "Item costs 25 dollars" # $inflated: "Item costs 28.75 dollars"
The original is preserved. The replacement was computed, not literal. Both flags doing their job simultaneously.

The order of the flags doesn't matter. s~~~er and s~~~re are identical. But er reads better because you naturally think "evaluate, then return."

Part 4: THE MAP CONNECTION

Here's where it gets interesting. Inside map, the default variable $_ is aliased to each element of the source list. If you mutate $_, you mutate the original.

Dangerous:

my @names = ('alice', 'bob', 'charlie'); my @upper = map { s~^(\w)~uc($1)~e; $_ } @names; # @names is NOW ('Alice', 'Bob', 'Charlie') -- MUTATED!
You changed the originals. That's almost never what you want in a map.

Safe:

my @names = ('alice', 'bob', 'charlie'); my @upper = map { s~^(\w)~uc($1)~er } @names; # @names is still ('alice', 'bob', 'charlie') # @upper is ('Alice', 'Bob', 'Charlie')
The /r flag makes it functional. No mutation. The map builds a new list from returned copies. The source list survives.

Part 5: PRICE DOUBLING

You have a list of price strings. Double every number in them:
my @prices = ( 'Widget: $25', 'Gadget: $50', 'Doohickey: $12.99', ); my @doubled = map { s~([\d.]+)~$1 * 2~ger } @prices;
Result:
Widget: $50 Gadget: $100 Doohickey: $25.98
The /g flag handles multiple numbers per string. The /e does the math. The /r protects the originals. Three flags, zero side effects.

Part 6: UNIT CONVERSION

Convert temperatures in text from Fahrenheit to Celsius:
my @readings = ( 'Lab A: 72F', 'Lab B: 68F', 'Lab C: 75F', ); my @celsius = map { s~(\d+)F~ sprintf("%.1fC", ($1 - 32) * 5/9) ~er } @readings;
Result:
Lab A: 22.2C Lab B: 20.0C Lab C: 23.9C
The replacement side is a full sprintf call. The /e flag doesn't care how complex your expression is. If it's valid Perl, it runs.

Convert bytes to human-readable sizes:

my @sizes = ('file1: 1048576', 'file2: 5368709120', 'file3: 1024'); my @human = map { s~(\d+)$~ my $b = $1; $b >= 1073741824 ? sprintf("%.1fG", $b/1073741824) : $b >= 1048576 ? sprintf("%.1fM", $b/1048576) : $b >= 1024 ? sprintf("%.1fK", $b/1024) : "${b}B" ~er } @sizes;
Result:
file1: 1.0M file2: 5.0G file3: 1.0K
A full conditional expression inside the replacement. Perl doesn't blink.

Part 7: DATE REFORMATTING

You have dates in MM/DD/YYYY and you want ISO 8601:
my @dates = ('01/15/2026', '12/25/2025', '03/29/2026'); my @iso = map { s~(\d{2})/(\d{2})/(\d{4})~"$3-$1-$2"~er } @dates;
Result:
2026-01-15 2025-12-25 2026-03-29
The captures $1, $2, $3 grab month, day, year. The replacement rearranges them. The /e evaluates the double-quoted string so the variables interpolate. The /r returns the copy.

Part 8: CHAINING WITH GREP

Combine map with grep for filter-and-transform pipelines:
my @log_lines = ( '2026-03-28 ERROR: disk usage 95%', '2026-03-28 INFO: backup complete', '2026-03-28 ERROR: memory usage 88%', '2026-03-28 INFO: cron job started', ); my @alerts = map { s~(\d+)%~"$1% (CRITICAL)" ~er } grep { m~ERROR~ } @log_lines;
Result:
2026-03-28 ERROR: disk usage 95% (CRITICAL) 2026-03-28 ERROR: memory usage 88% (CRITICAL)
Read bottom to top: filter for ERROR lines, then transform the percentage values. The original @log_lines is untouched. The output is a fresh list of modified copies.

Part 9: BUILDING DATA PIPELINES

Stack multiple transformations by nesting maps or chaining /r inside a single map:
my @raw = ( ' ALICE::Engineering::75000 ', ' BOB::Sales::62000 ', ' CHARLIE::Marketing::58000 ', ); my @formatted = map { s~^\s+|\s+$~~gr # trim =~ s~^(\w)(\w+)~$1 . lc($2)~er # title case name =~ s~::~, ~gr # delimiters to commas =~ s~(\d+)$~ sprintf('$%s', commify($1)) ~er # format salary } @raw; sub commify { my $n = reverse $_[0]; $n =~ s~(\d{3})(?=\d)~$1,~g; return scalar reverse $n; }
Result:
Alice, Engineering, $75,000 Bob, Sales, $62,000 Charlie, Marketing, $58,000
Each step in the chain uses /r to pass its result to the next. The /e flag on specific steps does the computation. The map collects the final results. Nothing is mutated.

Part 10: THE FUNCTIONAL ARGUMENT

Why does this matter? Because functional transforms are easier to reason about.

When you see this:

my @results = map { s~(\d+)~$1 * 2~er } @input;
You know three things immediately:
  1. @input is unchanged after this line
  2. @results contains modified copies
  3. The transformation is self-contained

No spooky action at a distance. No hidden mutation. No "wait, did the loop body change my source data?" The /r flag is a contract: this substitution is read-only with respect to the original.

Compare to the imperative approach:

my @results; for my $item (@input) { my $copy = $item; $copy =~ s~(\d+)~$1 * 2~e; push @results, $copy; }
Four lines to do what one line does. A manual copy. A mutable variable. A push. It all works, but it's ceremony around a simple idea.

The /er combo in map says what it means: transform and return. Nothing more.

@input | +---------+ | map | | s~~~er | +---------+ | @output Input unchanged. Output derived. Functional Perl. No mutation. .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
perl.gg